From 43032c7fd79e4340dd3607668a46050fc0a4f897 Mon Sep 17 00:00:00 2001 From: Eugenio Romano Date: Fri, 26 Jan 2018 16:35:25 +0000 Subject: [PATCH] 2.1.0 (#2891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adf-insights 2.0.1 * update script * [ADF-2048] Fixed corrupted link in User Guide (#2757) * [ADF-1752] disable completely sidebar when allowSidebar option is false (#2753) * [ADF-1752] disable completely sidebar when allowSidebar option is false * [ADF-1752] improved condition for showing toolbar * support for explicit viewer type for URL (#2688) * Fix line clamp temporary (#2747) * Adding automation-id to search-control component (#2759) * [ADF-2025] Task List - The pagination should be fetched from the UserPreference (#2733) [ADF-2025] Task List - The pagination should be fetched from the UserPreference * Update viewer documentation (#2763) * [ADF-1431] Destination Picker: Display path of the folder in search results (#2762) * Add initial implementation with style fixes * Further style fixes * Add tests, styling fixes * Fix the title of Model column in ECM License datatable. (#2770) * [ADF-1826] Task Header - The priority property should be editable (#2768) * [ADF-1826] Task Header - The priority property should be editable * [ADF-1826] - Fixed isEditble typo * [ADF-1826] - Adding unit test - checking the priority value is displayed * [ADF-1826] fixed unit test * [ADF-1826] fixed unit test (replaced fit with it) * [ADF-2056] update dependencies (#2764) * update dependencies add ban for fdescribe in tslint reenable test add badge codacy check bundles esm5 and esm2015 travis run new vuild script * remove fdescribe * material 5.0.0 stable release * comment out test service under should return the task details * [ADF-2051] Involve people name is not displayed properly. (#2773) This issue is now fixed * [ADF-2061] Fix content node selector search (#2772) * Fix content node selector search * Fix tests * [ADF-2069] Error is displayed on console when adding a description to a task. (#2779) * Fixed the console error coming while adding description or priority in task details * special CSS class if pagination is empty (#2780) * add missing exports (#2782) * [ADF-2065] Refactored Content node selector component (#2778) * [ADF-2065] created dialog component for content node selector * [ADF-2065] removing SiteModel from site dropdown to use SitePaging model of js-api * [ADF-2065] - removed site model and updated documentation * [ADF-2065] fixed test for site component * [ADF-2065] refactored content node selector and created content node selector dialog * [ADF-2065] fixed test on site-api service * [ADF-2065] added a new content node dialog service to centralise the logic for content node dialog * [ADF-2065] start adding test for node-actions service| * [ADF-2065] added test for node-actions service * [ADF-2065] added test for node action service * [ADF-2065] renamed components to keep backward compatibility * [ADF-2065] added input just for backward compatibility * [ADF-2065] added some changes for backward compatibility and updated documentation * [ADF-2065] updated documentation for content node selector * add missing exports (#2784) * [ADF-2102] Task List - Provide a way to support custom html template and static columns at same time (#2787) * * Removed unused adf-task-list keys from app.config * Fix can add/remove the static property through the app.config and can put custom columns into the html in adf-tasklist component * Fix add/remove the static property through the app.config and can put custom columns into the html in adf-tasklist component * * Fixed support custom html template and static columns. * Updated tasklist doc. * Fixed failing test case. * * Fixed support custom html template and static columns * * Updated task-list documentation. * [ADF-2094] Task Form - Doesn't follow the specification (#2786) * [ADF-2094] Task Form - Doesn't follow the specification * Removed icon visible beside title * Made font-size for title as semibold 20px * Set the outcome buttons on bottom right side * set color of Complete button as primary * [ADF-2094] Task Form - Doesn't follow the specification * Changed the css for the Adf-form according to zeplin design * Removed icon visible beside title * Made font-size for title as semibold 20px * Set the outcome buttons on bottom right side * set color of Complete button as primary * [ADF-2094] Task Form - Doesn't follow the specification * Changed the css for the adf-form according to zeplin design * Removed icon visible beside title * Made font-size for title as semibold 20px * Set the outcome buttons on bottom right side * set color of Complete button as primary * [ADF-2103] Info Drawer - Provide a way to select a different tab (#2788) * Provide a way to select a different tab * Fix the default value * Override the value through the html * fix different material depencncies (#2791) * [ADF-2070][ADF-2071][ADF-2072] 'Load more' on destination picker does… (#2774) * [ADF-2070][ADF-2071][ADF-2072] 'Load more' on destination picker doesn't display correct content of folders * [ADF-2070][ADF-2071][ADF-2072] add tests * [ADF-2070][ADF-2071][ADF-2072] 'Load more' on destination picker doesn't display correct content of folders * [ADF-2102] Task List - Provide a way to support custom html template and static columns at same time (#2793) * Removed lodash import from tasklist component. * Refactored getschema method. * [ADF-2067] Not able to see the 'My tasks' filter when clicking on a newly created app. (#2792) * fixed Not able to see the 'My tasks' filter when clicking on a newly created app. * Create default services return response with id. * [ADF-2098] Start Process - the default process definition should be selected (#2794) * [ADF-2098] Start Process - the default process definition should be selected * made the start-process component to automatically select Process definition if it is the only definition. * [ADF-2098] Start Process - the default process definition should be selected * made the start-process component to automatically select a processDef in start-form if there just one processDef present * [ADF-2098] Start Process - the default process definition should be selected * made the start-process component to automatically select a processDef in start-form if there just one processDef present * Update start-process.component.html * Update start-process.component.ts * Fix adf is not defined for customStencil (#2795) * [ADF-2109] Task Form - The form doesn't fit in the mobile screen (#2797) * [ADF-2109] Task Form - The form doesn't fit in the mobile screen * Made form mobile compatible * Fixed issues arising due to the issue ADF-2094 * [ADF-2109] Task Form - The form doesn't fit in the mobile screen * Made adf form mobile compatible * Fixed issues arising due to the issue ADF-2094 * [ADF-2095] Start Task - The start button should have the primary colour (#2799) * [ADF-2095] Start Task - The start button should have the primary colour. * fixed start button should have the primary colour. * [ADF-2095] Start Task/Process - The start button should have the primary colour. * fixed start button should have the primary colour and Uppercase. * * Start task button should have the primary colour * [ADF-1958] Process Attachment list - The empty list should be a simpl… (#2798) * [ADF-1958] Process Attachment list - The empty list should be a simple template * changed the default empty list to a simple template * changed process-attachment.component.html so that the user can pass custom empty tempate * [ADF-1958] Process Attachment list - The empty list should be a simple template * changed the default empty list to a simple template * changed process-attachment.component.html so that the user can pass custom empty tempate * removed some unused variables * [ADF-1958] Process Attachment list - The empty list should be a simple template * changed the default empty list to a simple template * changed process-attachment.component.html so that the user can pass custom empty tempate * [ADF-2119] Document List - The pagination should be fetched from the UserPreference (#2801) * Changed the service fetching page sizes in document-list component * uncommented a line in process-service.component.ts for the tasklist to show page-sizes in task-list * [no-issue] docker file (#2800) * docker file * cache busting for translation files (#2804) * [ADF-1999] i18n support for file size pipe (#2803) * i18n support for fileSize pipe * remove unused declaration * update unit test * fix documentation theming * docker nginx fix * documentation updates (#2806) * viewer styling improvements (#2805) * add missing headless chrome setup for Travis (#2807) * [ADF-2127] Started introducing new doc tools (#2809) * try to revert karma change * [fix-build] add stable version chrome in travis conf (#2819) * chrome stable * No input and No Output rename tslint rule (#2814) * [ADF-1999] fix file size translation for 0 bytes (#2817) * fix file size translation for 0 bytes * update translation mock * [ADF-2135] Updated tables of contents and tools (#2820) * [ADF-1986] Content matadata editing phase II. (#2796) * Aspects collection * Fetch only those metadata aspects which are defined in the application config * Aspect property filtering first round * Addig wildcard support for preset, default preset fallback to wildcard, and logging * Add white list service * Renaming services * ContentMetadataService and CardViewItemDispatcherComponent update * Observables... Observables everywhere... * Propers CardViewAspect * Defining more interfaces * Dummy data and expansions panels * Fix build attempt & proper panel expansion * Folder restructuring * Add different type mappings * Restructuring Card view component * Fix build * More ECM types supported * Validators first phase, extraction of interfaces, world domination preparation * Validators phase II. * Integer and float validators * Hide empty text items and validation message foundation * Validation error messages for text item view, small style changes * Update date item * bool item component * Datetimepicker npm module * Datetime model * Add mapping for datetime * Small fixes * Passing down preset * Adding forgotten package.json entry * Adding some tests for wrapper card component * content-metadata.component tests * Covering some edge cases, adding some tests * Fix cardview component's test * Add datetimepicker to demoshell * card view component show empty values by default * displayEmpty dependency injection * remove table like design from cardview * Using alfresco-js-api instead of spike * Remove spike folder and contents * Fix test * Cardview updated docs * Content metadata md * Fix review issues * Fix the packagr issue * [ADF-2097] Sidebar Action Menu - New component (#2813) SidebarMenu Component * [no-issue] downgrade scss bundle and some other minor fix (#2824) * downgrade scss bundle and some other minor fix * 2.0.9 * remove chrome in travis conf * execute different configuration * [ADF-1769] Added JSDoc for components (#2823) * [ADF-1769] Added JSDoc for components * [ADF-1769] Fixed tslint error with trailing whitespace * [ADF-2135] Updated tables of contents (#2822) * [ADF-2054] Created a new widget to handle uploading file from Share (#2810) * [ADF-2054] start creating custom upload widget for share integration * [ADF-2054] changed content node selector service to allow different opening approach * [ADF-2054] addedd support for multi resource files * [ADF-2054] fixed base case for upload when only local files are selected * [ADF-2054] start adding and fixing tests for new share attach button widget * [ADF-2054] changed test to perfrom a correct check * [ADF-2054] removed fdescribe * [ADF-2054] added test for share-widget component * [ADF-2054] added peer reviews changes * [ADF-2054] created a module folder for content widgets * [ADF-2054] fixed wrong import * [ADF-2054] fixed rebase errors * [ADF-2054] restored some files changed by rebase * [ADF-2054] added link to content services to fix packaging issue * [ADF-2054] renamed widget * [ADF-2136] support shared links and renditions in the Viewer (#2826) * support previewing shared link content * remove unused router instance * docs update * support 'shared links' preview in the Viewer * fallback to image preview if PDF rendition now available (#2827) * contentActions enabling properties * [contextMenuActions]="true" properties documentation * [ADF-2172]Adding data-automation-id for create task/process button (#2828) * [ADF-2152] Updated See Also sections in docs (#2830) * [ADF-1769] Updates to prop table script (#2829) * [ADF-1769] Fixed prop table tool formatting * [ADF-1769] Added code style to event type column * [ADF-2148] Update the angular and material libraries to the latest version (#2821) * Update the angular and material library to the latest version * [ADF-325] added attach folder widget (#2831) [ADF-325] added new attach folder widget * [ADF-2053] improved search result adding refresh on delete (#2836) * [ADF-2053] refreshed results when folder is deleted * [ADF-2053] improved search result adding refresh on delete * [ADF-2053] added missing whitespace * [ADF-2120] added page size retrieving from user preference on demo shell (#2837) * [ADF-2152] Added See Also sections and updated doc tools (#2835) * [ADF-2152] Added See Also sections to docs * [ADF-2152] Added doc script to package.json * [ADF-2162] Added screenshot for Notification service (#2843) * Fix mobile media query (#2840) * Remove wrapping form datatable header (#2841) * [ADF-2152] Updated See Also sections in docs (#2842) * [ADF-2055] Ability to pre-populate form with a file and values (#2834) * fix translations form * fix style webpack script * fix tslint error problem in viewer * fix naming problems in attach file widget * add start process form values data initialization option * fix translation problems * missing return type * start process name configuration * add CS cross PS configuration * start process from file example * fix minor issues * add documentation and move the dialog in a separate component * easy test select app * alfrescoRepositoryName right property * file conversion test * fix issue after CR * Remove forgotten semicolon. * Fix default translation key (#2844) * [ADF-1769] Added JSDocs and updated prop table script (#2838) * [ADF-1769] Added JSDocs and updated prop table script * [ADF-1769] Fixed lint error in JSDoc * [ADF-2152] Updated See Also sections in docs (#2846) * Add soem more explanation in the search autocomplete binding * Rollback the upgrade libs (#2850) * [ADF-1880] processDefinitionId parameter in adf-start-process (#2848) * processDefinitionId parameter in adf-start-process * fix test * fix test * Add @angular-devkit/core as dev dependency (#2853) * [ADF-2183] fixed translation on no permission tooltip (#2857) * Fix localisation in Datetimepicker (#2851) * [ADF-2157] Infinite pagination target supporting (#2856) * Infinite pagination target supporting * Updating documentation * [ADF-1769] Updated prop table script to handle inherited classes (#2855) * [ADF-2170] fixed condition for file size equal zero (#2860) * [ADF-2170] fixed condition for file size equal zero * [ADF-2170] refactored if condition * [ADF-2186] Update calculated properties on node change in content metadata compo… (#2862) * Update calculated properties on node change in content metadata component * Add module to public api * [ADF-2160] Improved combined action info in Content Action component docs (#2861) * remove processInstanceId reset (#2847) * [ADF-2150] improved queryBody mechanism (#2852) * [ADF-2150] changed query body parameter to a function * [ADF-2150] added an example page where to try change query body * [ADF-2150] improved queryBody mechanism * [ADF-2150] fixed content node test * [ADF-2150] extended docs added another way to use the query node * [ADF-2150] fixed test for search on content node * [ADF-2150] added some improvements to service config * [ADF-2150] changed the documentation accordingly * [ADF-2150] added PR changes * [ADF-2150] fixed jdoc * [ADF-2150] added checkbox to switch from service approach to input object approach * [ADF-2150] fixed build error on demo shell * style fixes for viewer (#2858) * style fixes for viewer * use theme colors * initial theme support for PDF viewer * [ADF-2196] fixed error message for no permission on copy and move (#2863) * [ADF-2196] fixed error message for no permission on copy and move * [ADF-2196] removed fdescribe * Fix missing translation (#2867) * Fix sidebar node reloading issue (#2866) [ADF-2186] Fix sidebar node reloading issue * [ADF-2201] Tidied Start Process docs (#2864) * [ADF-2201] Tidied Start Process docs * [ADF-2201] Removed file with probably incorrect name * [ADF-2174] Menus background color is not properly displayed in black themes (#2859) * [ADF-2174] Menus background color is not properly displayed in black themes * Removed global mat-menu-panel class style overriding * * Refactored styles * * Changed opacity property to rgba so that injected coponents will not be affected * Removed fixed menu width * [ADF-2201] Tidied Content Node Dialog docs (#2870) * Fix margin top (#2875) * Fix dynamic table css (#2879) * [ADF-1880] More configuration options for adf-start-process component (#2869) * procesdefinitionId set show dropdown multiple process * fix sourcemap * fix test start process * fix test * changes after code review * add show dropdown optional * remove fit fdescribe * processDefinitionId in processDefinition change * improve tests * new i18n file 2.1.0 (#2878) * [ADF-2201] Search Configuration doc (#2876) * [ADF-2201] Updated search component docs and tidied index page * [ADF-2201] Fixed incorrect link * [ADF-2201] Updated docs based on feedback * [ADF-2201] Fixed broken relative link * Update start-process.component.md * Update start-process.component.md * [ADF-2201] Updated renditions service docs (#2880) * [ADF-2208] The width of the 'New Task' option is smaller then Create Task drop down (#2881) * Set width for menu and button * [ADF-2200] fixed problem with redirection url (#2877) * [ADF-2200] fixed wrong handling of redirectUrl * [ADF-2200] fixed problem with redirection url * [ADF-2200] fixed redirection config path * [ADF-2200] fixed wrong fdescribe test * [ADF-2200] removed authserviceMock and fixed some tests * Fix css error (#2882) * [ADF-2188] Start Process - Change processDefinition property and fix bug (#2884) * Change processDefinition property and fix bug * Fix unit test * Fix async * Fix id * [ADF-2200] fixed redirectUrl check (#2886) * [ADF-2221] Search fails on Content Node Selector (#2888) * [ADF-2201] Fixed broken links (#2890) * export material only from the core (#2885) * Remove app.config reference from doc (#2889) * update versions to 2.1.0 --- .travis.yml | 19 +- Dockerfile | 6 + .../example_start_process_attach_file.zip | Bin 0 -> 7428 bytes demo-shell/.gitignore | 4 - demo-shell/nginx.conf | 25 + demo-shell/package.json | 54 +- demo-shell/resources/i18n/de.json | 17 +- demo-shell/resources/i18n/en.json | 9 +- demo-shell/resources/i18n/es.json | 17 +- demo-shell/resources/i18n/fr.json | 17 +- demo-shell/resources/i18n/it.json | 17 +- demo-shell/resources/i18n/ja.json | 17 +- demo-shell/resources/i18n/nb.json | 17 +- demo-shell/resources/i18n/nl.json | 17 +- demo-shell/resources/i18n/pt-BR.json | 17 +- demo-shell/resources/i18n/ru.json | 17 +- demo-shell/resources/i18n/zh-CN.json | 17 +- demo-shell/src/app.config.json | 60 +- demo-shell/src/app/app.component.ts | 6 +- demo-shell/src/app/app.module.ts | 6 +- demo-shell/src/app/app.routes.ts | 11 + .../app/components/about/about.component.html | 2 +- .../app/components/about/about.component.ts | 3 +- .../app-layout/app-layout.component.ts | 1 + .../file-view/file-view.component.html | 5 +- .../app/components/files/files.component.html | 24 +- .../app/components/files/files.component.ts | 45 +- ...sion-manager-dialog-adapter.component.html | 4 +- .../process-attachments.component.html | 22 +- .../process-service.component.html | 43 +- .../process-service.component.ts | 20 +- .../search/search-config-test.service.ts | 44 + .../search/search-extended.component.html | 39 + .../search/search-extended.component.scss | 62 ++ .../search/search-extended.component.ts | 84 ++ .../search/search-result.component.html | 3 +- .../search/search-result.component.ts | 4 + .../shared-link-view.component.html | 6 + .../shared-link-view.component.scss | 4 + .../shared-link-view.component.ts | 24 + demo-shell/src/polyfills.ts | 12 +- docs/FormFieldValidator.md | 9 +- docs/README.md | 424 +++++---- docs/accordion-group.component.md | 24 +- docs/accordion.component.md | 15 +- docs/activiti-alfresco.service.md | 3 +- docs/alfresco-api.service.md | 13 +- docs/analytics-generator.component.md | 11 +- docs/analytics-report-list.component.md | 17 +- docs/analytics.component.md | 11 +- docs/angular-material-design.md | 7 +- docs/app-config.service.md | 13 +- docs/apps-list.component.md | 15 +- docs/auth-guard-bpm.service.md | 10 +- docs/auth-guard-ecm.service.md | 10 +- docs/auth-guard.service.md | 10 +- docs/authentication.service.md | 16 +- docs/bpm-user.model.md | 9 +- docs/bpm-user.service.md | 7 +- docs/breadcrumb.component.md | 23 +- docs/card-view.component.md | 196 +++- docs/checklist.component.md | 18 +- docs/comment-list.component.md | 20 +- docs/comment-process.model.md | 5 +- docs/comment-process.service.md | 8 +- docs/comments.component.md | 14 +- docs/content-action.component.md | 128 ++- docs/content-metadata.component.md | 113 +++ docs/content-node-dialog.service.md | 69 ++ docs/content-node-selector-panel.component.md | 47 + docs/content-node-selector.component.md | 95 +- docs/content.service.md | 14 +- docs/content.widget.md | 20 +- docs/context-menu.directive.md | 15 +- docs/create-process-attachment.component.md | 12 +- docs/create-task-attachment.component.md | 4 +- docs/data-column.component.md | 63 +- docs/datatable.component.md | 76 +- .../images/ContentActSnackMessage.png | Bin 0 -> 3982 bytes docs/docassets/images/ContentMetadata.png | Bin 0 -> 42884 bytes docs/docassets/images/NotiService.png | Bin 0 -> 8296 bytes .../images/process-attachment-list.png | Bin 0 -> 13630 bytes docs/docassets/images/select-apps-dialog.png | Bin 0 -> 48484 bytes .../images/sidebar-action-menu-button.png | Bin 0 -> 7461 bytes .../images/sidebar-action-menu-icon.png | Bin 0 -> 9188 bytes docs/docassets/images/start_process.png | Bin 0 -> 86013 bytes docs/document-list.component.md | 282 +++--- docs/dropdown-breadcrumb.component.md | 16 +- docs/ecm-user.model.md | 9 +- docs/ecm-user.service.md | 7 +- docs/extensibility.md | 31 +- docs/favorites-api.service.md | 5 - docs/file-draggable.directive.md | 42 +- docs/file-size.pipe.md | 5 - docs/file-uploading-dialog.component.md | 11 +- docs/filter.model.md | 9 +- docs/folder-actions.service.md | 7 +- docs/folder-create.directive.md | 21 +- docs/folder-edit.directive.md | 13 +- docs/form-field.component.md | 13 +- docs/form-field.model.md | 42 +- docs/form-list.component.md | 17 +- docs/form-rendering.service.md | 14 +- docs/form.component.md | 82 +- docs/form.service.md | 50 +- docs/highlight-transform.service.md | 14 +- docs/highlight.directive.md | 11 +- docs/host-settings.component.md | 15 +- docs/infinite-pagination.component.md | 24 +- docs/info-drawer-layout.component.md | 11 +- docs/info-drawer.component.md | 8 +- docs/language-menu.component.md | 6 +- docs/like.component.md | 17 +- docs/login.component.md | 51 +- docs/logout.directive.md | 15 +- docs/mime-type-icon.pipe.md | 5 +- docs/node-delete.directive.md | 18 +- docs/node-name-tooltip.pipe.md | 27 +- docs/node-permission.directive.md | 30 +- docs/node-restore.directive.md | 20 +- docs/node.service.md | 58 +- docs/nodes-api.service.md | 27 +- docs/notification.service.md | 10 +- docs/page-title.service.md | 8 +- docs/pagination.component.md | 35 +- docs/people-content.service.md | 7 +- docs/people-list.component.md | 11 +- docs/people-process.service.md | 12 +- docs/people-search.component.md | 42 +- docs/people.component.md | 36 +- docs/permissions-style.model.md | 13 +- docs/process-attachment-list.component.md | 33 + docs/process-comments.component.md | 13 +- docs/process-content.service.md | 5 +- docs/process-filter.service.md | 123 ++- docs/process-filters.component.md | 17 +- docs/process-instance-details.component.md | 18 +- docs/process-instance-header.component.md | 18 +- docs/process-instance-tasks.component.md | 16 +- docs/process-list.component.md | 39 +- docs/process.service.md | 439 +++++---- docs/product-version.model.md | 5 +- docs/rating.component.md | 17 +- docs/rating.service.md | 7 +- docs/renditions.service.md | 44 +- docs/search-api.service.md | 5 - docs/search-configuration.interface.md | 88 ++ docs/search-configuration.service.md | 26 + docs/search-control.component.md | 34 +- docs/search.component.md | 52 +- docs/select-apps-dialog.component.md | 29 + docs/shared-links-api.service.md | 7 +- docs/sidebar-action-menu.component.md | 38 + docs/site.model.md | 5 +- docs/sites-dropdown.component.md | 2 +- ...{sites-api.service.md => sites.service.md} | 8 +- docs/start-form.component.md | 9 +- docs/start-process.component.md | 117 ++- docs/storage.service.md | 7 +- docs/summary.json | 2 +- docs/tag-actions.component.md | 10 +- docs/tag-list.component.md | 10 +- docs/tag-node-list.component.md | 10 +- docs/tag.service.md | 8 +- docs/task-details.model.md | 10 +- docs/task-filters.component.md | 24 +- docs/task-header.component.md | 14 +- docs/task-list.component.md | 113 ++- docs/tasklist.service.md | 435 +++++---- docs/text-highlight.pipe.md | 7 +- docs/text-mask.component.md | 17 +- docs/theming.md | 2 +- docs/thumbnail.service.md | 16 +- docs/time-ago.pipe.md | 5 - docs/toolbar-divider.component.md | 7 +- docs/toolbar-title.component.md | 5 + docs/toolbar.component.md | 29 +- docs/undocStoplist.json | 10 +- docs/upload-button.component.md | 2 + docs/upload.directive.md | 28 +- docs/user-info.component.md | 14 +- docs/user-initial.pipe.md | 9 +- docs/user-process.model.md | 5 +- docs/version-list.component.md | 10 +- docs/version-manager.component.md | 17 +- docs/viewer.component.md | 87 +- docs/webscript.component.md | 48 +- docs/widget.component.md | 7 +- lib/config/DocProcessor/docProcessor.js | 184 ++++ lib/config/DocProcessor/doctool.config.json | 5 + lib/config/DocProcessor/ngHelpers.js | 19 + lib/config/DocProcessor/tools/index.js | 362 ++++++++ lib/config/DocProcessor/tools/seealso.js | 175 ++++ lib/config/DocProcessor/tools/toc.js | 93 ++ lib/config/DocProcessor/tools/tscProps.js | 208 +++++ lib/config/DocProcessor/tools/tscProps.ts | 278 ++++++ lib/config/DocProcessor/unistHelpers.js | 122 +++ lib/config/karma.conf-all.js | 16 +- lib/config/webpack.coverage.js | 14 +- lib/config/webpack.test.js | 30 +- .../breadcrumb/breadcrumb.component.ts | 12 + .../content-metadata-card.component.html | 28 + .../content-metadata-card.component.scss | 13 +- .../content-metadata-card.component.spec.ts | 172 ++++ .../content-metadata-card.component.ts | 14 +- .../content-metadata.component.html | 43 + .../content-metadata.component.scss | 9 + .../content-metadata.component.spec.ts | 258 ++++++ .../content-metadata.component.ts | 61 +- .../content-metadata-card.component.html | 25 - .../content-metadata.component.html | 3 - .../content-metadata.component.scss | 29 - .../content-metadata.module.scss | 7 + .../content-metadata.module.ts | 20 +- .../interfaces/aspect-property.interface.ts | 26 + .../interfaces/aspect.interface.ts | 25 + .../interfaces/card-view-aspect.interface.ts | 23 + .../interfaces/content-metadata.interfaces.ts | 20 + .../content-metadata/public-api.ts | 4 +- .../services/aspect-whitelist.service.ts | 75 ++ .../basic-properties.service.ts} | 2 +- .../services/content-metadata.service.spec.ts | 325 +++++++ .../services/content-metadata.service.ts | 139 +++ .../properties-loader.service.spec.ts | 95 ++ .../services/properties-loader.service.ts | 36 + .../property-descriptors.service.spec.ts | 209 +++++ .../services/property-descriptors.service.ts | 60 ++ .../content-node-dialog.service.spec.ts | 130 +++ .../content-node-dialog.service.ts | 173 ++++ ...content-node-selector-panel.component.html | 95 ++ ...content-node-selector-panel.component.scss | 161 ++++ ...tent-node-selector-panel.component.spec.ts | 687 ++++++++++++++ .../content-node-selector-panel.component.ts | 318 +++++++ ...-node-selector.component-data.interface.ts | 11 +- .../content-node-selector.component.html | 116 +-- .../content-node-selector.component.scss | 165 +--- .../content-node-selector.component.spec.ts | 835 +++--------------- .../content-node-selector.component.ts | 347 +------- .../content-node-selector.module.ts | 16 +- .../content-node-selector.service.spec.ts | 106 +++ .../content-node-selector.service.ts | 20 +- .../name-location-cell.component.scss | 13 + .../name-location-cell.component.spec.ts | 65 ++ .../name-location-cell.component.ts | 43 + .../content-node-selector/public-api.ts | 2 + lib/content-services/content.module.ts | 3 +- .../dialogs/download-zip.dialog.ts | 41 +- .../content-action.component.spec.ts | 2 +- .../content-action.component.ts | 15 + .../content-column.component.ts | 2 +- .../components/document-list.component.ts | 55 +- .../services/document-actions.service.spec.ts | 2 +- .../services/folder-actions.service.spec.ts | 4 +- .../node-actions.service.service.spec.ts | 106 +++ .../services/node-actions.service.ts | 65 +- .../folder-create.directive.ts | 2 + .../folder-directive/folder-edit.directive.ts | 2 + lib/content-services/i18n/de.json | 39 +- lib/content-services/i18n/en.json | 22 +- lib/content-services/i18n/es.json | 39 +- lib/content-services/i18n/fr.json | 39 +- lib/content-services/i18n/it.json | 39 +- lib/content-services/i18n/ja.json | 39 +- lib/content-services/i18n/nb.json | 39 +- lib/content-services/i18n/nl.json | 39 +- lib/content-services/i18n/pt-BR.json | 39 +- lib/content-services/i18n/ru.json | 39 +- lib/content-services/i18n/zh-CN.json | 39 +- lib/content-services/material.module.ts | 2 + lib/content-services/package.json | 32 +- .../components/search-control.component.html | 5 +- .../components/search-control.component.ts | 1 + .../components/search-trigger.directive.ts | 2 + .../components/search.component.spec.ts | 17 +- .../search/components/search.component.ts | 77 +- .../sites-dropdown.component.html | 4 +- .../sites-dropdown.component.spec.ts | 26 +- .../site-dropdown/sites-dropdown.component.ts | 12 +- lib/content-services/styles/_index.scss | 12 +- .../upload-button.component.spec.ts | 11 + .../components/upload-button.component.ts | 14 +- .../directives/file-draggable.directive.ts | 2 + .../webscript/webscript.component.spec.ts | 28 +- lib/core/app-config/app-config.module.ts | 5 +- lib/core/app-config/app-config.service.ts | 3 +- .../app-config/startup-service-factory.ts | 22 + .../card-view-dateitem.component.html | 30 - .../card-view-dateitem.component.scss | 36 - .../card-view-textitem.component.html | 51 -- .../card-view-textitem.component.spec.ts | 233 ----- lib/core/card-view/card-view.component.html | 8 - lib/core/card-view/card-view.component.scss | 34 - lib/core/card-view/card-view.module.scss | 11 + lib/core/card-view/card-view.module.ts | 58 +- .../card-view-boolitem.component.html | 10 + .../card-view-boolitem.component.scss | 2 + .../card-view-boolitem.component.spec.ts | 243 +++++ .../card-view-boolitem.component.ts | 47 + .../card-view-dateitem.component.html | 37 + .../card-view-dateitem.component.scss | 41 + .../card-view-dateitem.component.spec.ts | 52 +- .../card-view-dateitem.component.ts | 34 +- ...ard-view-item-dispatcher.component.spec.ts | 52 +- .../card-view-item-dispatcher.component.ts | 22 +- .../card-view-mapitem.component.html | 6 +- .../card-view-mapitem.component.scss | 0 .../card-view-mapitem.component.spec.ts | 38 +- .../card-view-mapitem.component.ts | 11 +- .../card-view-textitem.component.html | 58 ++ .../card-view-textitem.component.scss | 45 +- .../card-view-textitem.component.spec.ts | 325 +++++++ .../card-view-textitem.component.ts | 38 +- .../components/card-view.components.ts | 23 + .../card-view/card-view.component.html | 11 + .../card-view/card-view.component.scss | 22 + .../card-view}/card-view.component.spec.ts | 86 +- .../card-view}/card-view.component.ts | 5 +- .../card-view-content-proxy.directive.ts | 0 ...card-view-boolitem-properties.interface.ts | 23 + ...card-view-dateitem-properties.interface.ts | 22 + .../card-view-item-properties.interface.ts | 28 + .../card-view-item-validator.interface.ts | 21 + .../interfaces}/card-view-item.interface.ts | 2 +- ...d-view-textitem-pipe-property.interface.ts | 23 + ...card-view-textitem-properties.interface.ts | 24 + .../interfaces/card-view.interfaces.ts | 24 + .../models/card-view-baseitem.model.spec.ts | 85 ++ .../models/card-view-baseitem.model.ts | 41 +- .../models/card-view-boolitem.model.spec.ts | 88 ++ .../models/card-view-boolitem.model.ts | 43 + .../models/card-view-dateitem.model.ts | 27 +- .../models/card-view-datetimeitem.model.ts | 25 + .../models/card-view-floatitem.model.spec.ts | 59 ++ .../models/card-view-floatitem.model.ts | 35 + .../models/card-view-intitem.model.spec.ts | 59 ++ .../models/card-view-intitem.model.ts | 35 + .../models/card-view-mapitem.model.ts | 18 +- .../models/card-view-textitem.model.spec.ts | 29 +- .../models/card-view-textitem.model.ts | 32 +- lib/core/card-view/models/card-view.models.ts | 25 + lib/core/card-view/public-api.ts | 19 +- .../services/card-item-types.service.ts | 13 +- .../services/card-view-update.service.spec.ts | 0 .../services/card-view-update.service.ts | 0 .../card-view/services/card-view.services.ts | 19 + .../card-view-item-float.validator.ts | 27 + .../card-view-item-int.validator.ts | 27 + .../validators/card-view.validators.ts | 19 + .../collapsable/accordion-group.component.ts | 7 + .../context-menu/context-menu.directive.ts | 2 + lib/core/core.module.ts | 3 + lib/core/data-column/data-column.component.ts | 19 +- .../datatable/datatable.component.scss | 1 + .../datatable/datatable.component.ts | 25 + .../datatable/filesize-cell.component.ts | 3 +- lib/core/directives/highlight.directive.ts | 2 + lib/core/directives/node-delete.directive.ts | 2 + .../directives/node-favorite.directive.ts | 2 + .../directives/node-permission.directive.ts | 2 + lib/core/directives/node-restore.directive.ts | 2 + lib/core/directives/upload.directive.ts | 2 + .../form-field/form-field.component.ts | 4 + lib/core/form/components/form.component.html | 10 +- lib/core/form/components/form.component.scss | 150 +++- .../form/components/form.component.spec.ts | 4 + lib/core/form/components/form.component.ts | 6 + .../form.component.visibility.spec.ts | 6 +- .../widgets/amount/amount.widget.scss | 2 +- .../widgets/attach/attach.widget.css | 15 - .../widgets/attach/attach.widget.html | 32 - .../widgets/attach/attach.widget.spec.ts | 302 ------- .../widgets/attach/attach.widget.ts | 156 ---- .../widgets/checkbox/checkbox.widget.ts | 2 +- .../form/components/widgets/core/index.ts | 2 + .../widgets/dropdown/dropdown.widget.spec.ts | 2 +- .../dynamic-table/dynamic-table.widget.scss | 5 + .../dynamic-table.widget.spec.ts | 4 +- lib/core/form/components/widgets/index.ts | 3 - .../widgets/number/number.widget.ts | 2 +- .../widgets/text/text-mask.component.ts | 2 +- .../upload-folder/upload-folder.widget.html | 7 + .../upload-folder/upload-folder.widget.scss | 12 + .../upload-folder.widget.spec.ts | 16 + .../upload-folder/upload-folder.widget.ts | 150 ++++ .../widgets/upload/upload.widget.ts | 2 +- lib/core/form/public-api.ts | 1 + .../services/activiti-alfresco.service.ts | 34 +- .../services/form-rendering.service.spec.ts | 24 +- .../form/services/form-rendering.service.ts | 16 +- lib/core/i18n/de.json | 63 +- lib/core/i18n/en.json | 41 + lib/core/i18n/es.json | 65 +- lib/core/i18n/fr.json | 63 +- lib/core/i18n/it.json | 63 +- lib/core/i18n/ja.json | 63 +- lib/core/i18n/nb.json | 63 +- lib/core/i18n/nl.json | 63 +- lib/core/i18n/pt-BR.json | 63 +- lib/core/i18n/ru.json | 63 +- lib/core/i18n/zh-CN.json | 63 +- lib/core/index.ts | 1 + .../info-drawer/info-drawer.component.html | 4 +- .../info-drawer/info-drawer.component.spec.ts | 28 +- lib/core/info-drawer/info-drawer.component.ts | 3 + lib/core/interface/public-api.ts | 2 +- .../search-configuration.interface.ts | 24 + .../login/components/login.component.spec.ts | 27 +- lib/core/login/components/login.component.ts | 18 +- lib/core/mock/traslation.service.mock.ts | 8 +- lib/core/models/public-api.ts | 6 +- lib/core/models/redirection.model.ts | 33 + lib/core/models/site.model.ts | 91 -- lib/core/package.json | 30 +- .../infinite-pagination.component.spec.ts | 155 ++-- .../infinite-pagination.component.ts | 35 +- .../paginated-component.interface.ts | 6 +- .../pagination/pagination.component.spec.ts | 13 +- lib/core/pagination/pagination.component.ts | 8 +- lib/core/pipes/file-size.pipe.spec.ts | 18 +- lib/core/pipes/file-size.pipe.ts | 15 +- lib/core/services/alfresco-api.service.ts | 6 +- .../services/auth-guard-bpm.service.spec.ts | 83 +- lib/core/services/auth-guard-bpm.service.ts | 20 +- .../services/auth-guard-ecm.service.spec.ts | 4 +- lib/core/services/auth-guard-ecm.service.ts | 17 +- lib/core/services/auth-guard.service.spec.ts | 70 +- lib/core/services/auth-guard.service.ts | 22 +- .../services/authentication.service.spec.ts | 69 +- lib/core/services/authentication.service.ts | 17 +- lib/core/services/content.service.ts | 20 +- .../dynamic-component-mapper.service.ts | 5 +- lib/core/services/favorites-api.service.ts | 25 +- lib/core/services/get-type.ts | 20 + lib/core/services/public-api.ts | 3 +- lib/core/services/renditions.service.ts | 2 +- .../services/search-configuration.service.ts | 45 + lib/core/services/search.service.ts | 19 +- lib/core/services/service.module.ts | 8 +- ...ites-api.spec.ts => sites.service.spec.ts} | 4 +- lib/core/services/sites.service.ts | 28 +- lib/core/services/thumbnail.service.ts | 4 +- lib/core/services/translate-loader.service.ts | 8 +- lib/core/services/translation.service.ts | 4 + .../services/user-preferences.service.spec.ts | 11 +- lib/core/services/user-preferences.service.ts | 10 +- lib/core/sidebar/index.ts | 18 + lib/core/sidebar/public-api.ts | 18 + .../sidebar-action-menu.component.html | 16 + .../sidebar-action-menu.component.scss | 63 ++ .../sidebar-action-menu.component.spec.ts | 149 ++++ .../sidebar/sidebar-action-menu.component.ts | 44 + lib/core/sidebar/sidebar-action.module.ts | 38 + lib/core/styles/_index.scss | 15 +- lib/core/styles/_mixins.scss | 76 +- .../components/user-info.component.spec.ts | 107 ++- .../components/pdfViewer.component.html | 7 +- .../components/pdfViewer.component.scss | 77 +- .../components/pdfViewerHost.component.scss | 10 +- .../components/txtViewer.component.scss | 1 - .../viewer/components/viewer.component.html | 8 +- .../viewer/components/viewer.component.scss | 23 + .../components/viewer.component.spec.ts | 12 + .../viewer/components/viewer.component.ts | 156 +++- ...lytics-report-parameters.component.spec.ts | 2 +- .../widgets/checkbox/checkbox.widget.ts | 11 +- .../widgets/date-range/date-range.widget.ts | 2 + .../widgets/dropdown/dropdown.widget.ts | 3 +- .../widgets/duration/duration.widget.ts | 3 +- .../widgets/number/number.widget.ts | 3 +- .../diagram.component.boundary.spec.ts | 4 +- .../diagram.component.events.spec.ts | 4 +- .../components/diagram.component.swim.spec.ts | 6 +- .../diagram.component.throw.spec.ts | 35 +- .../diagram/services/diagrams.service.spec.ts | 84 ++ lib/insights/index.ts | 6 +- lib/insights/insights.module.ts | 1 - lib/insights/package.json | 32 +- lib/ng-package/ng-package-core.json | 2 + lib/ng-package/ng-package-process.json | 1 + lib/package.json | 63 +- .../app-list/apps-list.component.html | 2 +- .../app-list/apps-list.component.scss | 14 + .../app-list/apps-list.component.spec.ts | 2 +- .../app-list/apps-list.component.ts | 6 + .../app-list/apps-list.module.ts | 10 +- lib/process-services/app-list/public-api.ts | 1 + .../select-apps-dialog-component.html | 13 + .../select-apps-dialog-component.spec.ts | 131 +++ .../app-list/select-apps-dialog-component.ts | 56 ++ .../assets/images/alfresco-flower.svg | 51 ++ .../create-process-attachment.component.ts | 7 + .../create-task-attachment.component.ts | 7 + .../process-attachment-list.component.html | 30 +- .../process-attachment-list.component.spec.ts | 62 +- .../process-attachment-list.component.ts | 25 +- .../task-attachment-list.component.spec.ts | 10 +- .../task-attachment-list.component.ts | 7 +- .../comments/comment-list.component.ts | 2 + .../comments/comments.component.ts | 3 + .../attach-file-widget.component.html | 83 ++ .../attach-file-widget.component.scss | 73 ++ .../attach-file-widget.component.ts | 142 +++ .../attach-file-widget.components.spec.ts | 243 +++++ .../attach-folder-widget.component.html | 30 + .../attach-folder-widget.component.scss | 39 + .../attach-folder-widget.component.spec.ts | 169 ++++ .../attach-folder-widget.component.ts | 89 ++ .../content-widget/content-widget.module.ts | 52 ++ lib/process-services/content-widget/index.ts | 18 + .../content-widget/public-api.ts | 19 + lib/process-services/i18n/de.json | 18 +- lib/process-services/i18n/en.json | 15 + lib/process-services/i18n/es.json | 18 +- lib/process-services/i18n/fr.json | 18 +- lib/process-services/i18n/it.json | 18 +- lib/process-services/i18n/ja.json | 18 +- lib/process-services/i18n/nb.json | 18 +- lib/process-services/i18n/nl.json | 18 +- lib/process-services/i18n/pt-BR.json | 18 +- lib/process-services/i18n/ru.json | 18 +- lib/process-services/i18n/zh-CN.json | 18 +- lib/process-services/material.module.ts | 4 +- .../process/start-process.component.mock.ts | 8 +- lib/process-services/package.json | 33 +- .../people/people.component.scss | 10 +- .../components/process-audit.directive.ts | 2 + .../process-instance-header.component.ts | 15 +- .../process-instance-tasks.component.ts | 3 - .../components/process-list.component.css | 6 +- .../components/start-process.component.html | 25 +- .../components/start-process.component.scss | 19 +- .../start-process.component.spec.ts | 393 ++++++--- .../components/start-process.component.ts | 127 ++- .../process-list/process-list.module.ts | 4 +- .../services/process-filter.service.spec.ts | 61 +- .../services/process-filter.service.ts | 3 + .../process-list/services/process.service.ts | 4 +- lib/process-services/process.module.ts | 3 +- .../components/checklist.component.ts | 10 + .../no-task-detail-template.directive.spec.ts | 3 +- .../components/start-task.component.html | 2 +- .../components/start-task.component.scss | 20 +- .../components/task-audit.directive.ts | 2 + .../components/task-details.component.ts | 7 +- .../components/task-header.component.spec.ts | 8 + .../components/task-header.component.ts | 18 +- .../components/task-list.component.css | 6 +- .../components/task-list.component.spec.ts | 4 +- .../components/task-list.component.ts | 79 +- lib/process-services/task-list/public-api.ts | 8 +- .../services/task-filter.service.spec.ts | 131 ++- .../task-list/services/task-filter.service.ts | 4 + .../services/tasklist.service.spec.ts | 552 +++++------- .../task-list/task-list.module.ts | 4 +- lib/tsconfig.json | 1 + lib/tslint.json | 9 +- scripts/import-langs.sh | 4 +- scripts/next_version.sh | 4 +- scripts/npm-build-all.sh | 6 +- scripts/npm-check-bundles.sh | 14 + scripts/update-version.sh | 8 +- tslint.json | 69 +- 562 files changed, 16123 insertions(+), 6889 deletions(-) create mode 100644 Dockerfile create mode 100644 assets/ps-example-apps/example_start_process_attach_file.zip create mode 100644 demo-shell/nginx.conf create mode 100644 demo-shell/src/app/components/search/search-config-test.service.ts create mode 100644 demo-shell/src/app/components/search/search-extended.component.html create mode 100644 demo-shell/src/app/components/search/search-extended.component.scss create mode 100644 demo-shell/src/app/components/search/search-extended.component.ts create mode 100644 demo-shell/src/app/components/shared-link-view/shared-link-view.component.html create mode 100644 demo-shell/src/app/components/shared-link-view/shared-link-view.component.scss create mode 100644 demo-shell/src/app/components/shared-link-view/shared-link-view.component.ts create mode 100644 docs/content-metadata.component.md create mode 100644 docs/content-node-dialog.service.md create mode 100644 docs/content-node-selector-panel.component.md create mode 100644 docs/docassets/images/ContentActSnackMessage.png create mode 100644 docs/docassets/images/ContentMetadata.png create mode 100644 docs/docassets/images/NotiService.png create mode 100644 docs/docassets/images/process-attachment-list.png create mode 100644 docs/docassets/images/select-apps-dialog.png create mode 100644 docs/docassets/images/sidebar-action-menu-button.png create mode 100644 docs/docassets/images/sidebar-action-menu-icon.png create mode 100644 docs/docassets/images/start_process.png create mode 100644 docs/search-configuration.interface.md create mode 100644 docs/search-configuration.service.md create mode 100644 docs/select-apps-dialog.component.md create mode 100644 docs/sidebar-action-menu.component.md rename docs/{sites-api.service.md => sites.service.md} (89%) create mode 100644 lib/config/DocProcessor/docProcessor.js create mode 100644 lib/config/DocProcessor/doctool.config.json create mode 100644 lib/config/DocProcessor/ngHelpers.js create mode 100644 lib/config/DocProcessor/tools/index.js create mode 100644 lib/config/DocProcessor/tools/seealso.js create mode 100644 lib/config/DocProcessor/tools/toc.js create mode 100644 lib/config/DocProcessor/tools/tscProps.js create mode 100644 lib/config/DocProcessor/tools/tscProps.ts create mode 100644 lib/config/DocProcessor/unistHelpers.js create mode 100644 lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.html rename lib/content-services/content-metadata/{ => components/content-metadata-card}/content-metadata-card.component.scss (68%) create mode 100644 lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts rename lib/content-services/content-metadata/{ => components/content-metadata-card}/content-metadata-card.component.ts (85%) create mode 100644 lib/content-services/content-metadata/components/content-metadata/content-metadata.component.html create mode 100644 lib/content-services/content-metadata/components/content-metadata/content-metadata.component.scss create mode 100644 lib/content-services/content-metadata/components/content-metadata/content-metadata.component.spec.ts rename lib/content-services/content-metadata/{ => components/content-metadata}/content-metadata.component.ts (52%) delete mode 100644 lib/content-services/content-metadata/content-metadata-card.component.html delete mode 100644 lib/content-services/content-metadata/content-metadata.component.html delete mode 100644 lib/content-services/content-metadata/content-metadata.component.scss create mode 100644 lib/content-services/content-metadata/content-metadata.module.scss create mode 100644 lib/content-services/content-metadata/interfaces/aspect-property.interface.ts create mode 100644 lib/content-services/content-metadata/interfaces/aspect.interface.ts create mode 100644 lib/content-services/content-metadata/interfaces/card-view-aspect.interface.ts create mode 100644 lib/content-services/content-metadata/interfaces/content-metadata.interfaces.ts create mode 100644 lib/content-services/content-metadata/services/aspect-whitelist.service.ts rename lib/content-services/content-metadata/{content-metadata.service.ts => services/basic-properties.service.ts} (98%) create mode 100644 lib/content-services/content-metadata/services/content-metadata.service.spec.ts create mode 100644 lib/content-services/content-metadata/services/content-metadata.service.ts create mode 100644 lib/content-services/content-metadata/services/properties-loader.service.spec.ts create mode 100644 lib/content-services/content-metadata/services/properties-loader.service.ts create mode 100644 lib/content-services/content-metadata/services/property-descriptors.service.spec.ts create mode 100644 lib/content-services/content-metadata/services/property-descriptors.service.ts create mode 100644 lib/content-services/content-node-selector/content-node-dialog.service.spec.ts create mode 100644 lib/content-services/content-node-selector/content-node-dialog.service.ts create mode 100644 lib/content-services/content-node-selector/content-node-selector-panel.component.html create mode 100644 lib/content-services/content-node-selector/content-node-selector-panel.component.scss create mode 100644 lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts create mode 100644 lib/content-services/content-node-selector/content-node-selector-panel.component.ts create mode 100644 lib/content-services/content-node-selector/content-node-selector.service.spec.ts create mode 100644 lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.scss create mode 100644 lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.spec.ts create mode 100644 lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.ts create mode 100644 lib/content-services/document-list/services/node-actions.service.service.spec.ts create mode 100644 lib/core/app-config/startup-service-factory.ts delete mode 100644 lib/core/card-view/card-view-dateitem.component.html delete mode 100644 lib/core/card-view/card-view-dateitem.component.scss delete mode 100644 lib/core/card-view/card-view-textitem.component.html delete mode 100644 lib/core/card-view/card-view-textitem.component.spec.ts delete mode 100644 lib/core/card-view/card-view.component.html delete mode 100644 lib/core/card-view/card-view.component.scss create mode 100644 lib/core/card-view/card-view.module.scss create mode 100644 lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.html create mode 100644 lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.scss create mode 100644 lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.spec.ts create mode 100644 lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.ts create mode 100644 lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.html create mode 100644 lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.scss rename lib/core/card-view/{ => components/card-view-dateitem}/card-view-dateitem.component.spec.ts (79%) rename lib/core/card-view/{ => components/card-view-dateitem}/card-view-dateitem.component.ts (66%) rename lib/core/card-view/{ => components/card-view-item-dispatcher}/card-view-item-dispatcher.component.spec.ts (71%) rename lib/core/card-view/{ => components/card-view-item-dispatcher}/card-view-item-dispatcher.component.ts (76%) rename lib/core/card-view/{ => components/card-view-mapitem}/card-view-mapitem.component.html (63%) rename lib/core/card-view/{ => components/card-view-mapitem}/card-view-mapitem.component.scss (100%) rename lib/core/card-view/{ => components/card-view-mapitem}/card-view-mapitem.component.spec.ts (77%) rename lib/core/card-view/{ => components/card-view-mapitem}/card-view-mapitem.component.ts (79%) create mode 100644 lib/core/card-view/components/card-view-textitem/card-view-textitem.component.html rename lib/core/card-view/{ => components/card-view-textitem}/card-view-textitem.component.scss (66%) create mode 100644 lib/core/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts rename lib/core/card-view/{ => components/card-view-textitem}/card-view-textitem.component.ts (63%) create mode 100644 lib/core/card-view/components/card-view.components.ts create mode 100644 lib/core/card-view/components/card-view/card-view.component.html create mode 100644 lib/core/card-view/components/card-view/card-view.component.scss rename lib/core/card-view/{ => components/card-view}/card-view.component.spec.ts (61%) rename lib/core/card-view/{ => components/card-view}/card-view.component.ts (88%) rename lib/core/card-view/{ => directives}/card-view-content-proxy.directive.ts (100%) create mode 100644 lib/core/card-view/interfaces/card-view-boolitem-properties.interface.ts create mode 100644 lib/core/card-view/interfaces/card-view-dateitem-properties.interface.ts create mode 100644 lib/core/card-view/interfaces/card-view-item-properties.interface.ts create mode 100644 lib/core/card-view/interfaces/card-view-item-validator.interface.ts rename lib/core/{interface => card-view/interfaces}/card-view-item.interface.ts (96%) create mode 100644 lib/core/card-view/interfaces/card-view-textitem-pipe-property.interface.ts create mode 100644 lib/core/card-view/interfaces/card-view-textitem-properties.interface.ts create mode 100644 lib/core/card-view/interfaces/card-view.interfaces.ts create mode 100644 lib/core/card-view/models/card-view-baseitem.model.spec.ts rename lib/core/{ => card-view}/models/card-view-baseitem.model.ts (59%) create mode 100644 lib/core/card-view/models/card-view-boolitem.model.spec.ts create mode 100644 lib/core/card-view/models/card-view-boolitem.model.ts rename lib/core/{ => card-view}/models/card-view-dateitem.model.ts (66%) create mode 100644 lib/core/card-view/models/card-view-datetimeitem.model.ts create mode 100644 lib/core/card-view/models/card-view-floatitem.model.spec.ts create mode 100644 lib/core/card-view/models/card-view-floatitem.model.ts create mode 100644 lib/core/card-view/models/card-view-intitem.model.spec.ts create mode 100644 lib/core/card-view/models/card-view-intitem.model.ts rename lib/core/{ => card-view}/models/card-view-mapitem.model.ts (69%) rename lib/core/{ => card-view}/models/card-view-textitem.model.spec.ts (63%) rename lib/core/{ => card-view}/models/card-view-textitem.model.ts (62%) create mode 100644 lib/core/card-view/models/card-view.models.ts rename lib/core/{ => card-view}/services/card-item-types.service.ts (59%) rename lib/core/{ => card-view}/services/card-view-update.service.spec.ts (100%) rename lib/core/{ => card-view}/services/card-view-update.service.ts (100%) create mode 100644 lib/core/card-view/services/card-view.services.ts create mode 100644 lib/core/card-view/validators/card-view-item-float.validator.ts create mode 100644 lib/core/card-view/validators/card-view-item-int.validator.ts create mode 100644 lib/core/card-view/validators/card-view.validators.ts delete mode 100644 lib/core/form/components/widgets/attach/attach.widget.css delete mode 100644 lib/core/form/components/widgets/attach/attach.widget.html delete mode 100644 lib/core/form/components/widgets/attach/attach.widget.spec.ts delete mode 100644 lib/core/form/components/widgets/attach/attach.widget.ts create mode 100644 lib/core/form/components/widgets/upload-folder/upload-folder.widget.html create mode 100644 lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss create mode 100644 lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts create mode 100644 lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts create mode 100644 lib/core/interface/search-configuration.interface.ts create mode 100644 lib/core/models/redirection.model.ts delete mode 100644 lib/core/models/site.model.ts create mode 100644 lib/core/services/get-type.ts create mode 100644 lib/core/services/search-configuration.service.ts rename lib/core/services/{sites-api.spec.ts => sites.service.spec.ts} (97%) create mode 100644 lib/core/sidebar/index.ts create mode 100644 lib/core/sidebar/public-api.ts create mode 100644 lib/core/sidebar/sidebar-action-menu.component.html create mode 100644 lib/core/sidebar/sidebar-action-menu.component.scss create mode 100644 lib/core/sidebar/sidebar-action-menu.component.spec.ts create mode 100644 lib/core/sidebar/sidebar-action-menu.component.ts create mode 100644 lib/core/sidebar/sidebar-action.module.ts create mode 100644 lib/insights/diagram/services/diagrams.service.spec.ts create mode 100644 lib/process-services/app-list/select-apps-dialog-component.html create mode 100644 lib/process-services/app-list/select-apps-dialog-component.spec.ts create mode 100644 lib/process-services/app-list/select-apps-dialog-component.ts create mode 100644 lib/process-services/assets/images/alfresco-flower.svg create mode 100644 lib/process-services/content-widget/attach-file-widget.component.html create mode 100644 lib/process-services/content-widget/attach-file-widget.component.scss create mode 100644 lib/process-services/content-widget/attach-file-widget.component.ts create mode 100644 lib/process-services/content-widget/attach-file-widget.components.spec.ts create mode 100644 lib/process-services/content-widget/attach-folder-widget.component.html create mode 100644 lib/process-services/content-widget/attach-folder-widget.component.scss create mode 100644 lib/process-services/content-widget/attach-folder-widget.component.spec.ts create mode 100644 lib/process-services/content-widget/attach-folder-widget.component.ts create mode 100644 lib/process-services/content-widget/content-widget.module.ts create mode 100644 lib/process-services/content-widget/index.ts create mode 100644 lib/process-services/content-widget/public-api.ts diff --git a/.travis.yml b/.travis.yml index 9ded3af2a3..a3e5f89fbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: node_js dist: trusty -sudo: false +sudo: required node_js: - "8" +addons: + chrome: stable + before_install: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" + - export CHROME_BIN=chromium-browser + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start env: matrix: @@ -15,6 +19,7 @@ env: - MODULE=process-services - MODULE=content-services - MODULE=demo-shell + - MODULE=packaging install: - if ([ "$MODULE" != "demo-shell" ]); then @@ -27,7 +32,7 @@ install: script: - if ([ "$MODULE" == "core" ]); then - (./scripts/npm-build-all.sh -si -sb -t "core*" || exit 1;); + (./scripts/npm-build-all.sh -si -sb -t "core" || exit 1;); fi - if ([ "$MODULE" == "process-services" ]); then @@ -42,6 +47,10 @@ script: (./scripts/npm-build-all.sh -si -sb -t "content-services" || exit 1;); fi + - if ([ "$MODULE" == "packaging" ]); then + (cd lib && npm run new-build || exit 1;); + fi + - if ([ "$MODULE" == "demo-shell" ]); then if ([ "$TRAVIS_BRANCH" = "master" ]); then (./scripts/start.sh -t -ss || exit 1;); @@ -80,7 +89,5 @@ after_success: cache: directories: - - ./node_modules/material-design-lite - - ./node_modules/material-design-icons - demo-shell/node_modules - lib/node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..474eb72d44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx + +COPY demo-shell/nginx.conf /etc/nginx/nginx.conf + +WORKDIR /usr/share/nginx/html +COPY demo-shell/dist/ . diff --git a/assets/ps-example-apps/example_start_process_attach_file.zip b/assets/ps-example-apps/example_start_process_attach_file.zip new file mode 100644 index 0000000000000000000000000000000000000000..c827929b0506c99a77ddb169e7eeeb446196b8aa GIT binary patch literal 7428 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

Server settings

- The values below are taken from the AppConfigService and loaded from the '{{ configFile }}' file. + The values below are taken from the AppConfigService

Alfresco Process Services URL: {{ bpmHost }}

diff --git a/demo-shell/src/app/components/about/about.component.ts b/demo-shell/src/app/components/about/about.component.ts index cd2546ccc4..0c0e26334e 100644 --- a/demo-shell/src/app/components/about/about.component.ts +++ b/demo-shell/src/app/components/about/about.component.ts @@ -39,7 +39,6 @@ export class AboutComponent implements OnInit { modules: ObjectDataTableAdapter; githubUrlCommitAlpha = 'https://github.com/Alfresco/alfresco-ng2-components/commits/'; - configFile = 'app.config.json'; ecmHost = ''; bpmHost = ''; @@ -80,7 +79,7 @@ export class AboutComponent implements OnInit { {type: 'text', key: 'expiresAt', title: 'Expires At', sortable: true}, {type: 'text', key: 'remainingDays', title: 'Remaining Days', sortable: true}, {type: 'text', key: 'holder', title: 'Holder', sortable: true}, - {type: 'text', key: 'mode', title: 'Is Cluster Enabled', sortable: true}, + {type: 'text', key: 'mode', title: 'Mode', sortable: true}, {type: 'text', key: 'isClusterEnabled', title: 'Is Cluster Enabled', sortable: true}, {type: 'text', key: 'isCryptodocEnabled', title: 'Is Cryptodoc Enable', sortable: true} ]); diff --git a/demo-shell/src/app/components/app-layout/app-layout.component.ts b/demo-shell/src/app/components/app-layout/app-layout.component.ts index fc3c044d31..1fef973b98 100644 --- a/demo-shell/src/app/components/app-layout/app-layout.component.ts +++ b/demo-shell/src/app/components/app-layout/app-layout.component.ts @@ -41,6 +41,7 @@ export class AppLayoutComponent { { href: '/tag', icon: 'local_offer', title: 'APP_LAYOUT.TAG' }, { href: '/social', icon: 'thumb_up', title: 'APP_LAYOUT.SOCIAL' }, { href: '/settings', icon: 'settings', title: 'APP_LAYOUT.SETTINGS' }, + { href: '/extendedSearch', icon: 'search', title: 'APP_LAYOUT.SEARCH' }, { href: '/overlay-viewer', icon: 'pageview', title: 'APP_LAYOUT.OVERLAY_VIEWER' }, { href: '/about', icon: 'info_outline', title: 'APP_LAYOUT.ABOUT' } ]; diff --git a/demo-shell/src/app/components/file-view/file-view.component.html b/demo-shell/src/app/components/file-view/file-view.component.html index 7625eb1e0f..f7afcaf3b2 100644 --- a/demo-shell/src/app/components/file-view/file-view.component.html +++ b/demo-shell/src/app/components/file-view/file-view.component.html @@ -9,7 +9,8 @@ - Versions go here... + + @@ -49,4 +50,4 @@ --> - \ No newline at end of file + diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 1fcfe16e6b..facb0ca9d7 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -191,6 +191,7 @@ format="timeAgo" class="desktop-only"> + @@ -222,6 +223,7 @@ [disableWithNoPermission]="true" title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}" (permissionEvent)="handlePermissionError($event)" + (success)="onDeleteActionSuccess($event)" handler="delete"> @@ -267,6 +269,13 @@ (success)="onDeleteActionSuccess($event)" handler="delete"> + + + [target]="documentList" + [loading]="documentList.infiniteLoading"> {{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }} +
@@ -316,6 +325,13 @@ +
+ + +
+

Current folder ID: {{ documentList.currentFolderId }}

diff --git a/demo-shell/src/app/components/files/files.component.ts b/demo-shell/src/app/components/files/files.component.ts index 92ab592679..3c349d56ac 100644 --- a/demo-shell/src/app/components/files/files.component.ts +++ b/demo-shell/src/app/components/files/files.component.ts @@ -21,16 +21,18 @@ import { } from '@angular/core'; import { MatDialog } from '@angular/material'; import { ActivatedRoute, Params, Router } from '@angular/router'; -import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { MinimalNodeEntity, NodePaging, Pagination, MinimalNodeEntryEntity, SiteEntry } from 'alfresco-js-api'; import { - AlfrescoApiService, ContentService, TranslationService, + AlfrescoApiService, AuthenticationService, ContentService, TranslationService, FileUploadEvent, FolderCreatedEvent, LogService, NotificationService, - SiteModel, UploadService, DataColumn, DataRow, UserPreferencesService, - PaginationComponent + UploadService, DataColumn, DataRow, UserPreferencesService, + PaginationComponent, FormValues } from '@alfresco/adf-core'; import { DocumentListComponent, PermissionStyleModel, DownloadZipDialogComponent } from '@alfresco/adf-content-services'; +import { SelectAppsDialogComponent } from '@alfresco/adf-process-services'; + import { VersionManagerDialogAdapterComponent } from './version-manager-dialog-adapter.component'; import { Subscription } from 'rxjs/Rx'; @@ -60,6 +62,10 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { // The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root- currentFolderId: string = DEFAULT_FOLDER_TO_SHOW; + formValues: FormValues = {}; + + processId; + @Input() selectionMode = 'multiple'; @@ -117,6 +123,9 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { @Output() loadNext: EventEmitter = new EventEmitter(); + @Output() + deleteElementSuccess: EventEmitter = new EventEmitter(); + @ViewChild(DocumentListComponent) documentList: DocumentListComponent; @@ -125,6 +134,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { permissionsStyle: PermissionStyleModel[] = []; infiniteScrolling: boolean; + supportedPages: number[]; private onCreateFolder: Subscription; private onEditFolder: Subscription; @@ -139,7 +149,8 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { private router: Router, private logService: LogService, private preference: UserPreferencesService, - @Optional() private route: ActivatedRoute) { + @Optional() private route: ActivatedRoute, + public authenticationService: AuthenticationService) { } showFile(event) { @@ -182,6 +193,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { this.contentService.folderCreated.subscribe(value => this.onFolderCreated(value)); this.onCreateFolder = this.contentService.folderCreate.subscribe(value => this.onFolderAction(value)); this.onEditFolder = this.contentService.folderEdit.subscribe(value => this.onFolderAction(value)); + this.supportedPages = this.preference.getDifferentPageSizes(); // this.permissionsStyle.push(new PermissionStyleModel('document-list__create', PermissionsEnum.CREATE)); // this.permissionsStyle.push(new PermissionStyleModel('document-list__disable', PermissionsEnum.NOT_CREATE, false, true)); @@ -203,7 +215,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { } giveDefaultPaginationWhenNotDefined() { - this.pagination = { + this.pagination = { maxItems: this.preference.paginationSize, skipCount: 0, totalItems: 0, @@ -251,7 +263,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { } handlePermissionError(event: any) { - this.translateService.get('OPERATION.ERROR.NO_PERMISSION_EVENT', { + this.translateService.get('VERSION.NO_PERMISSION_EVENT', { permission: event.permission, action: event.action, type: event.type @@ -308,6 +320,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { onDeleteActionSuccess(message) { this.uploadService.fileDeleted.next(message); + this.deleteElementSuccess.emit(); } onManageVersions(event) { @@ -325,8 +338,8 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { } } - getSiteContent(site: SiteModel) { - this.currentFolderId = site && site.guid ? site.guid : DEFAULT_FOLDER_TO_SHOW; + getSiteContent(site: SiteEntry) { + this.currentFolderId = site && site.entry.guid ? site.entry.guid : DEFAULT_FOLDER_TO_SHOW; } getDocumentListCurrentFolderId() { @@ -440,6 +453,20 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { return false; } + startProcesAction($event) { + this.formValues['file'] = $event.value.entry; + + const dialogRef = this.dialog.open(SelectAppsDialogComponent, { + width: '630px', + panelClass: 'adf-version-manager-dialog' + }); + + dialogRef.afterClosed().subscribe(selectedProcess => { + this.processId = selectedProcess.id; + }); + + } + onChangePageSize(event: Pagination): void { this.preference.paginationSize = event.maxItems; this.changedPageSize.emit(event); diff --git a/demo-shell/src/app/components/files/version-manager-dialog-adapter.component.html b/demo-shell/src/app/components/files/version-manager-dialog-adapter.component.html index e1decb8bd4..062fbf32a4 100644 --- a/demo-shell/src/app/components/files/version-manager-dialog-adapter.component.html +++ b/demo-shell/src/app/components/files/version-manager-dialog-adapter.component.html @@ -1,7 +1,7 @@ -
Manage versions
+
{{'VERSION.DIALOG.TITLE' | translate}}
- +
diff --git a/demo-shell/src/app/components/process-service/process-attachments.component.html b/demo-shell/src/app/components/process-service/process-attachments.component.html index 61670f6fcd..7955be82a2 100644 --- a/demo-shell/src/app/components/process-service/process-attachments.component.html +++ b/demo-shell/src/app/components/process-service/process-attachments.component.html @@ -6,10 +6,24 @@ [disabled]="isCompletedProcess()"> + *ngIf="processInstanceId" + [disabled]="isCompletedProcess()" + [processInstanceId]="processInstanceId" + (attachmentClick)="onAttachmentClick($event)"> + +
+ {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.HEADER' | translate}} +
+
+
+ {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.DRAG-AND-DROP.TITLE' | translate}} +
+
+ {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.DRAG-AND-DROP.SUBTITLE' | translate}} +
+
+ +
diff --git a/demo-shell/src/app/components/process-service/process-service.component.html b/demo-shell/src/app/components/process-service/process-service.component.html index 74d2e07e01..bd82e5e4da 100644 --- a/demo-shell/src/app/components/process-service/process-service.component.html +++ b/demo-shell/src/app/components/process-service/process-service.component.html @@ -4,10 +4,14 @@
- + + arrow_drop_down +
+ +
+
+ [supportedPageSizes]="supportedPages">
@@ -105,15 +109,14 @@
- + + arrow_drop_down +
+ +
+
diff --git a/demo-shell/src/app/components/process-service/process-service.component.ts b/demo-shell/src/app/components/process-service/process-service.component.ts index e2d4b817aa..57c678f0ea 100644 --- a/demo-shell/src/app/components/process-service/process-service.component.ts +++ b/demo-shell/src/app/components/process-service/process-service.component.ts @@ -30,7 +30,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Pagination, ProcessInstanceFilterRepresentation } from 'alfresco-js-api'; import { FORM_FIELD_VALIDATORS, FormEvent, FormFieldEvent, FormRenderingService, FormService, - DynamicTableRow, ValidateDynamicTableRowEvent + DynamicTableRow, ValidateDynamicTableRowEvent, AppConfigService } from '@alfresco/adf-core'; import { AnalyticsReportListComponent } from '@alfresco/adf-insights'; @@ -52,7 +52,7 @@ import { TaskListService } from '@alfresco/adf-process-services'; import { LogService } from '@alfresco/adf-core'; -import { AlfrescoApiService } from '@alfresco/adf-core'; +import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core'; import { DataSorting, ObjectDataRow, @@ -121,6 +121,11 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit taskPage = 0; processSchemaColumns: any[] = []; + supportedPages: number[]; + + defaultProcessDefinitionName: string; + defaultProcessName: string; + activeTab: number = this.tabs.tasks; // tasks|processes|reports taskFilter: FilterRepresentationModel; @@ -146,10 +151,17 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit private taskListService: TaskListService, private apiService: AlfrescoApiService, private logService: LogService, + private appConfig: AppConfigService, formRenderingService: FormRenderingService, - formService: FormService) { + formService: FormService, + private preferenceService: UserPreferencesService) { this.dataTasks = new ObjectDataTableAdapter(); this.dataTasks.setSorting(new DataSorting('created', 'desc')); + this.supportedPages = this.preferenceService.getDifferentPageSizes(); + this.taskPagination.maxItems = this.preferenceService.paginationSize; + + this.defaultProcessName = this.appConfig.get('adf-start-process.name'); + this.defaultProcessDefinitionName = this.appConfig.get('adf-start-process.processDefinitionName'); // Uncomment this line to replace all 'text' field editors with custom component // formRenderingService.setComponentTypeResolver('text', () => CustomEditorComponent, true); @@ -200,6 +212,7 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit this.taskPage = this.currentPage(skipCount, maxItems); this.taskPagination.maxItems = maxItems; this.taskPagination.skipCount = skipCount; + this.preferenceService.paginationSize = maxItems; } onChangePageNumber(pagination: Pagination): void { @@ -288,7 +301,6 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit } onProcessFilterClick(event: ProcessInstanceFilterRepresentation): void { - this.currentProcessInstanceId = null; this.processFilter = event; } diff --git a/demo-shell/src/app/components/search/search-config-test.service.ts b/demo-shell/src/app/components/search/search-config-test.service.ts new file mode 100644 index 0000000000..42683447c3 --- /dev/null +++ b/demo-shell/src/app/components/search/search-config-test.service.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { QueryBody } from 'alfresco-js-api'; +import { SearchConfigurationInterface } from '@alfresco/adf-core'; + +export class TestSearchConfigurationService implements SearchConfigurationInterface { + + constructor() { + } + + public generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody { + const defaultQueryBody: QueryBody = { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: maxResults, + skipCount: skipCount + }, + filterQueries: [ + /*tslint:disable-next-line */ + { query: "TYPE:'cm:folder'" }, + { query: 'NOT cm:creator:System' }] + }; + + return defaultQueryBody; + } +} diff --git a/demo-shell/src/app/components/search/search-extended.component.html b/demo-shell/src/app/components/search/search-extended.component.html new file mode 100644 index 0000000000..5fe9ee9ed7 --- /dev/null +++ b/demo-shell/src/app/components/search/search-extended.component.html @@ -0,0 +1,39 @@ +
+

{{'APP_LAYOUT.EXTENDED_SEARCH_QUERY_BODY' | translate}}

+ + {{'APP_LAYOUT.SEARCH_SERVICE_APPROACH' | translate}} + + +
+ + + + +
+
+ + + + +
+ {{ item?.entry.name }} + {{ item?.entry.createdAt }} +
+ + +

+ {{'APP_LAYOUT.SEARCH_CREATED_BY' | translate}}: {{item?.entry.createdByUser?.displayName}} +

+
+
+ +

{{ 'SEARCH.RESULTS.NONE' | translate:{searchTerm: searchTerm} }}

+
+
+
+
+
diff --git a/demo-shell/src/app/components/search/search-extended.component.scss b/demo-shell/src/app/components/search/search-extended.component.scss new file mode 100644 index 0000000000..386a94a880 --- /dev/null +++ b/demo-shell/src/app/components/search/search-extended.component.scss @@ -0,0 +1,62 @@ +div.search-results-container { + padding: 0 20px 20px 20px; +} + +.adf-search-title { + font-size: 22px; + padding: 15px 0 15px 0; +} + +@media screen and (max-width: 600px) { + :host .col-display-name { + min-width: 100px; + } + :host .col-modified-at, :host .col-modified-by { + display: none; + } + :host div.search-results-container table { + width: 100%; + } +} + +.adf-search-results-content{ + display: flex; +} + +.search-extended-input-containers { + display: flex; + flex-direction: row-reverse; + justify-content: space-evenly; +} + +.search-extended-input-textarea { + width: 300px; +} + +.search-extended-label-error { + display: flex; + flex-direction: column; +} + +.example-card { + width: 200px; + flex: 0 20%; + margin: 15px; + } + + .example-card-search-container { + display: flex; + flex-wrap: wrap; + } + + .example-header-image { + + background-size: cover; + margin-bottom: 15px; + } + + .example-search-input { + width: 100px; + border: 1 solid; + border-color: black; + } diff --git a/demo-shell/src/app/components/search/search-extended.component.ts b/demo-shell/src/app/components/search/search-extended.component.ts new file mode 100644 index 0000000000..8f6842b746 --- /dev/null +++ b/demo-shell/src/app/components/search/search-extended.component.ts @@ -0,0 +1,84 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Component, ViewChild, ViewEncapsulation } from '@angular/core'; +import { NodePaging, Pagination, QueryBody, MinimalNodeEntity } from 'alfresco-js-api'; +import { SearchComponent } from '@alfresco/adf-content-services'; +import { ThumbnailService } from '@alfresco/adf-core'; +import { SearchService, SearchConfigurationService } from '@alfresco/adf-core'; +import { TestSearchConfigurationService } from './search-config-test.service'; + +@Component({ + selector: 'app-search-extended-component', + templateUrl: './search-extended.component.html', + styleUrls: ['./search-extended.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [ + { provide: SearchConfigurationService, useClass: TestSearchConfigurationService }, + SearchService + ] +}) +export class SearchExtendedComponent { + + @ViewChild('search') + search: SearchComponent; + + queryParamName = 'q'; + searchedWord = ''; + queryBodyString = ''; + errorMessage = ''; + resultNodePageList: NodePaging; + maxItems: number; + skipCount = 0; + pagination: Pagination; + queryBody: QueryBody; + useServiceApproach = false; + + constructor(public thumbnailService: ThumbnailService) { + + } + + getMimeTypeIcon(node: MinimalNodeEntity): string { + let mimeType; + + if (node.entry.content && node.entry.content.mimeType) { + mimeType = node.entry.content.mimeType; + } + if (node.entry.isFolder) { + mimeType = 'folder'; + } + + return this.thumbnailService.getMimeTypeIcon(mimeType); + } + + generateQueryBody(searchTerm: string): QueryBody { + if (this.useServiceApproach) { + return null; + } else { + return { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + filterQueries: [ + /*tslint:disable-next-line */ + { query: "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { query: 'NOT cm:creator:System' }] + }; + } + } +} diff --git a/demo-shell/src/app/components/search/search-result.component.html b/demo-shell/src/app/components/search/search-result.component.html index 913bf2b5e1..e8f461e9e1 100644 --- a/demo-shell/src/app/components/search/search-result.component.html +++ b/demo-shell/src/app/components/search/search-result.component.html @@ -14,5 +14,6 @@ (changedPageNumber)="onRefreshPagination($event)" (turnedNextPage)="onRefreshPagination($event)" (loadNext)="onRefreshPagination($event)" - (turnedPreviousPage)="onRefreshPagination($event)"> + (turnedPreviousPage)="onRefreshPagination($event)" + (deleteElementSuccess)="onDeleteElementSuccess($event)"> diff --git a/demo-shell/src/app/components/search/search-result.component.ts b/demo-shell/src/app/components/search/search-result.component.ts index 81a8914f43..7b7288a946 100644 --- a/demo-shell/src/app/components/search/search-result.component.ts +++ b/demo-shell/src/app/components/search/search-result.component.ts @@ -62,4 +62,8 @@ export class SearchResultComponent implements OnInit { this.maxItems = pagination.maxItems; this.skipCount = pagination.skipCount; } + + onDeleteElementSuccess(element: any) { + this.search.reload(); + } } diff --git a/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html new file mode 100644 index 0000000000..07da8ce05d --- /dev/null +++ b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.html @@ -0,0 +1,6 @@ + + + + diff --git a/demo-shell/src/app/components/shared-link-view/shared-link-view.component.scss b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.scss new file mode 100644 index 0000000000..39982fce39 --- /dev/null +++ b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.scss @@ -0,0 +1,4 @@ +.app-shared-link-view { + width: 100%; + height: 100%; +} diff --git a/demo-shell/src/app/components/shared-link-view/shared-link-view.component.ts b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.ts new file mode 100644 index 0000000000..056301dca9 --- /dev/null +++ b/demo-shell/src/app/components/shared-link-view/shared-link-view.component.ts @@ -0,0 +1,24 @@ +import { Component, ViewEncapsulation, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'app-shared-link-view', + templateUrl: 'shared-link-view.component.html', + styleUrls: [ 'shared-link-view.component.scss' ], + encapsulation: ViewEncapsulation.None, + // tslint:disable-next-line:use-host-property-decorator + host: { 'class': 'app-shared-link-view' } +}) +export class SharedLinkViewComponent implements OnInit { + + sharedLinkId: string = null; + + constructor(private route: ActivatedRoute) {} + + ngOnInit() { + this.route.params.subscribe(params => { + this.sharedLinkId = params.id; + }); + } + +} diff --git a/demo-shell/src/polyfills.ts b/demo-shell/src/polyfills.ts index fdf0d3805b..1fbf2b074f 100644 --- a/demo-shell/src/polyfills.ts +++ b/demo-shell/src/polyfills.ts @@ -34,11 +34,10 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ - import 'classlist.js'; // Run `npm install --save classlist.js`. +import 'classlist.js'; // Run `npm install --save classlist.js`. /** IE10 and IE11 requires the following to support `@angular/animation`. */ - import 'web-animations-js'; // Run `npm install --save web-animations-js`. - +import 'web-animations-js'; // Run `npm install --save web-animations-js`. /** Evergreen browsers require these. **/ import 'core-js/es6/reflect'; @@ -47,18 +46,11 @@ import 'core-js/es6/array'; import 'core-js/es7/array'; -/** ALL Firefox browsers require the following to support `@angular/animation`. **/ -import 'web-animations-js'; // Run `npm install --save web-animations-js`. - - - /*************************************************************************************************** * Zone JS is required by Angular itself. */ import 'zone.js/dist/zone'; // Included with Angular CLI. - - /*************************************************************************************************** * APPLICATION IMPORTS */ diff --git a/docs/FormFieldValidator.md b/docs/FormFieldValidator.md index 54108e2b8f..133d3fd32f 100644 --- a/docs/FormFieldValidator.md +++ b/docs/FormFieldValidator.md @@ -56,7 +56,7 @@ during the validation phase. Several validator classes are predefined for you to use: | Validator name | Checks that: | -| --- | --- | +| -------------- | ------------ | | `RequiredFieldValidator` | Field is not left blank | | `NumberFieldValidator` | Field contains numeric data | | `MinLengthFieldValidator` | Field text has at least a minimum number of characters | @@ -144,10 +144,7 @@ If you now run the application and try to enter "admin" in one of the text field ![](docassets/images/demo-validator.png) - - ## See also -- [Form field model](form-field.model.md) -- [Form component](form.component.md) - \ No newline at end of file +- [Form field model](form-field.model.md) +- [Form component](form.component.md) diff --git a/docs/README.md b/docs/README.md index 37776d8f24..eae8aa1192 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,27 +2,30 @@ Below is an index of the documentation for ADF. The [User Guide](#user-guide) section discusses particular techniques in depth. The other sections are references for the ADF -libraries. Items marked with an asterisk (*) do not currently have documentation - the link leads -to the appropriate source file. +libraries. Click the name of an item to see its documentation or click the source link to see +its main source file. Note that ADF is developed continuously, so the source files for some items +may be listed here before their documentation is available. ## Contents -- [User Guide](#user-guide) -- [ADF Core](#adf-core) -- [ADF Content Services](#adf-content-services) -- [ADF Process Services](#adf-process-services) -- [ADF Insights](#adf-insights) +- [User Guide](#user-guide) +- [ADF Core](#adf-core) +- [ADF Content Services](#adf-content-services) +- [ADF Process Services](#adf-process-services) +- [ADF Insights](#adf-insights) ## User guide - -- [Form Extensibility and Customisation](extensibility.md) -- [Angular Material Design](angular-material-design.md) -- [Theming](theming.md) -- [Typography](typography.md) -- [Walkthrough: adding indicators to highlight information about a node](metadata-indicators.md) + + +- [Form Extensibility and Customisation](extensibility.md) +- [Angular Material Design](angular-material-design.md) +- [Theming](theming.md) +- [Typography](typography.md) +- [Walkthrough - adding indicators to highlight information about a node](metadata-indicators.md) + + - [(Back to Contents)](#contents) ## ADF Core @@ -31,112 +34,128 @@ Contains a variety of components used throughout ADF. See the library's [README file](../lib/core/README.md) for more information about installing and using the source code. - -### Components + -- [Card view component](card-view.component.md) -- [Accordion group component](accordion-group.component.md) -- [Accordion component](accordion.component.md) -- [Data column component](data-column.component.md) -- [Datatable component](datatable.component.md) -- [Form field component](form-field.component.md) -- [Form list component](form-list.component.md) -- [Form component](form.component.md) -- [Start form component](start-form.component.md) -- [Text mask component](text-mask.component.md) -- [Widget component](widget.component.md) -- [Info drawer layout component](info-drawer-layout.component.md) -- [Info drawer component](info-drawer.component.md) -- [Language menu component](language-menu.component.md) -- [Login component](login.component.md) -- [Infinite pagination component](infinite-pagination.component.md) -- [Pagination component](pagination.component.md) -- [Host settings component](host-settings.component.md) -- [Toolbar divider component](toolbar-divider.component.md) -- [Toolbar title component](toolbar-title.component.md) -- [Toolbar component](toolbar.component.md) -- [User info component](user-info.component.md) -- [Viewer component](viewer.component.md) +## Components -### Directives +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Card view component](card-view.component.md) | Displays a configurable property list renderer. | [Source](../lib/core/card-view/components/card-view/card-view.component.ts) | +| [Accordion group component](accordion-group.component.md) | Adds a collapsible panel to an [accordion menu](accordion.component.md). | [Source](../lib/core/collapsable/accordion-group.component.ts) | +| [Accordion component](accordion.component.md) | Creates a collapsible accordion menu. | [Source](../lib/core/collapsable/accordion.component.ts) | +| [Data column component](data-column.component.md) | Defines column properties for DataTable, Tasklist, Document List and other components. | [Source](../lib/core/data-column/data-column.component.ts) | +| [Datatable component](datatable.component.md) | Displays data as a table with customizable columns and presentation. | [Source](../lib/core/datatable/components/datatable/datatable.component.ts) | +| [Form field component](form-field.component.md) | A form field in an APS form. | [Source](../lib/core/form/components/form-field/form-field.component.ts) | +| [Form list component](form-list.component.md) | Shows APS forms as a list. | [Source](../lib/core/form/components/form-list.component.ts) | +| [Form component](form.component.md) | Shows a Form from APS (see it live: [Form Quickstart](https://embed.plnkr.co/YSLXTqb3DtMhVJSqXKkE/)) | [Source](../lib/core/form/components/form.component.ts) | +| [Start form component](start-form.component.md) | Displays the Start Form for a process. | [Source](../lib/core/form/components/start-form.component.ts) | +| [Text mask component](text-mask.component.md) | Implements text field input masks. | [Source](../lib/core/form/components/widgets/text/text-mask.component.ts) | +| [Info drawer layout component](info-drawer-layout.component.md) | Displays a sidebar-style information panel. | [Source](../lib/core/info-drawer/info-drawer-layout.component.ts) | +| [Info drawer component](info-drawer.component.md) | Displays a sidebar-style information panel with tabs. | [Source](../lib/core/info-drawer/info-drawer.component.ts) | +| [Language menu component](language-menu.component.md) | Displays all the languages that are present in the "app.config.json" or the default one (EN). | [Source](../lib/core/language-menu/language-menu.component.ts) | +| [Login component](login.component.md) | Authenticates to Alfresco Content Services and Alfresco Process Services. | [Source](../lib/core/login/components/login.component.ts) | +| [Infinite pagination component](infinite-pagination.component.md) | Adds "infinite" pagination to the component it is used with. | [Source](../lib/core/pagination/infinite-pagination.component.ts) | +| [Pagination component](pagination.component.md) | Adds pagination to the component it is used with. | [Source](../lib/core/pagination/pagination.component.ts) | +| [Host settings component](host-settings.component.md) | Validates the URLs for ACS and APS and saves them in the user's local storage | [Source](../lib/core/settings/host-settings.component.ts) | +| [Sidebar action menu component](sidebar-action-menu.component.md) | Displays a sidebar-action menu information panel. | [Source](../lib/core/sidebar/sidebar-action-menu.component.ts) | +| [Toolbar divider component](toolbar-divider.component.md) | Divides groups of elements in a Toolbar with a visual separator. | [Source](../lib/core/toolbar/toolbar-divider.component.ts) | +| [Toolbar title component](toolbar-title.component.md) | Supplies custom HTML to be included in a Toolbar component title. | [Source](../lib/core/toolbar/toolbar-title.component.ts) | +| [Toolbar component](toolbar.component.md) | Simple container for headers, titles, actions and breadcrumbs. | [Source](../lib/core/toolbar/toolbar.component.ts) | +| [User info component](user-info.component.md) | Shows user information. | [Source](../lib/core/userinfo/components/user-info.component.ts) | +| [Viewer component](viewer.component.md) | Displays content from an ACS repository. | [Source](../lib/core/viewer/components/viewer.component.ts) | -- [Context menu directive](context-menu.directive.md) -- [Highlight directive](highlight.directive.md) -- [Logout directive](logout.directive.md) -- [Node delete directive](node-delete.directive.md) -- [Node favorite directive](node-favorite.directive.md) -- [Node permission directive](node-permission.directive.md) -- [Node restore directive](node-restore.directive.md) -- [Upload directive](upload.directive.md) +## Directives -### Models +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Context menu directive](context-menu.directive.md) | Adds a context menu to a component. | [Source](../lib/core/context-menu/context-menu.directive.ts) | +| [Highlight directive](highlight.directive.md) | Adds highlighting to selected sections of an HTML element's content. | [Source](../lib/core/directives/highlight.directive.ts) | +| [Logout directive](logout.directive.md) | Logs the user out when the decorated element is clicked. | [Source](../lib/core/directives/logout.directive.ts) | +| [Node delete directive](node-delete.directive.md) | Deletes multiple files and folders. | [Source](../lib/core/directives/node-delete.directive.ts) | +| [Node favorite directive](node-favorite.directive.md) | Selectively toggles nodes as favorite | [Source](../lib/core/directives/node-favorite.directive.ts) | +| [Node permission directive](node-permission.directive.md) | Selectively disables an HTML element or Angular component | [Source](../lib/core/directives/node-permission.directive.ts) | +| [Node restore directive](node-restore.directive.md) | Restores deleted nodes to their original location. | [Source](../lib/core/directives/node-restore.directive.ts) | +| [Upload directive](upload.directive.md) | Allows your components or common HTML elements reacting on File drag and drop in order to upload content. | [Source](../lib/core/directives/upload.directive.ts) | -- [Form field model](form-field.model.md) -- [Comment process model](comment-process.model.md) -- [Product version model](product-version.model.md) -- [Site model](site.model.md) -- [User process model](user-process.model.md) -- [Bpm user model](bpm-user.model.md) -- [Ecm user model](ecm-user.model.md) +## Models -### Pipes +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Form field model](form-field.model.md) | Contains the value and metadata for a field of an [ADF Form](form.component.md). | [Source](../lib/core/form/components/widgets/core/form-field.model.ts) | +| [Comment process model](comment-process.model.md) | Represents a comment added to a Process Services task or process instance. | [Source](../lib/core/models/comment-process.model.ts) | +| [Product version model](product-version.model.md) | Contains version and license information classes for Alfresco products. | [Source](../lib/core/models/product-version.model.ts) | +| [User process model](user-process.model.md) | Represents a Process Services user. | [Source](../lib/core/models/user-process.model.ts) | +| [Bpm user model](bpm-user.model.md) | Contains information about a Process Services user. | [Source](../lib/core/userinfo/models/bpm-user.model.ts) | +| [Ecm user model](ecm-user.model.md) | Contains information about a Content Services user. | [Source](../lib/core/userinfo/models/ecm-user.model.ts) | -- [File size pipe](file-size.pipe.md) -- [Mime type icon pipe](mime-type-icon.pipe.md) -- [Node name tooltip pipe](node-name-tooltip.pipe.md) -- [Text highlight pipe](text-highlight.pipe.md) -- [Time ago pipe](time-ago.pipe.md) -- [User initial pipe](user-initial.pipe.md) +## Pipes -### Services +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [File size pipe](file-size.pipe.md) | Converts a number of bytes to the equivalent in KB, MB, etc. | [Source](../lib/core/pipes/file-size.pipe.ts) | +| [Mime type icon pipe](mime-type-icon.pipe.md) | Retrieves an icon to represent a MIME type. | [Source](../lib/core/pipes/mime-type-icon.pipe.ts) | +| [Node name tooltip pipe](node-name-tooltip.pipe.md) | Formats the tooltip for a Node. | [Source](../lib/core/pipes/node-name-tooltip.pipe.ts) | +| [Text highlight pipe](text-highlight.pipe.md) | Adds highlighting to words or sections of text that match a search string. | [Source](../lib/core/pipes/text-highlight.pipe.ts) | +| [Time ago pipe](time-ago.pipe.md) | Converts a recent past date into a number of days ago. | [Source](../lib/core/pipes/time-ago.pipe.ts) | +| [User initial pipe](user-initial.pipe.md) | Takes the name fields of a UserProcessModel object and extracts and formats the initials. | [Source](../lib/core/pipes/user-initial.pipe.ts) | -- [App config service](app-config.service.md) -- [Activiti alfresco service](activiti-alfresco.service.md) -- [Form rendering service](form-rendering.service.md) -- [Form service](form.service.md) -- [Node service](node.service.md) -- [Process content service](process-content.service.md) -- [Alfresco api service](alfresco-api.service.md) -- [Apps process service](apps-process.service.md) -- [Auth guard bpm service](auth-guard-bpm.service.md) -- [Auth guard ecm service](auth-guard-ecm.service.md) -- [Auth guard service](auth-guard.service.md) -- [Authentication service](authentication.service.md) -- [Card view update service](card-view-update.service.md) -- [Comment process service](comment-process.service.md) -- [Content service](content.service.md) -- [Cookie service](cookie.service.md) -- [Deleted nodes api service](deleted-nodes-api.service.md) -- [Discovery api service](discovery-api.service.md) -- [Favorites api service](favorites-api.service.md) -- [Highlight transform service](highlight-transform.service.md) -- [Log service](log.service.md) -- [Nodes api service](nodes-api.service.md) -- [Notification service](notification.service.md) -- [Page title service](page-title.service.md) -- [People content service](people-content.service.md) -- [People process service](people-process.service.md) -- [Renditions service](renditions.service.md) -- [Shared links api service](shared-links-api.service.md) -- [Storage service](storage.service.md) -- [Thumbnail service](thumbnail.service.md) -- [Translation service](translation.service.md) -- [Upload service](upload.service.md) -- [User preferences service](user-preferences.service.md) -- [Bpm user service](bpm-user.service.md) -- [Ecm user service](ecm-user.service.md) -- [*Sites service](../lib/core/services/sites.service.ts) +## Services -### Widgets +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Card view update service](card-view-update.service.md) | Reports edits and clicks within fields of a [Card View component](card-view.component.md). | [Source](../lib/core/card-view/services/card-view-update.service.ts) | +| [Activiti alfresco service](activiti-alfresco.service.md) | Gets Alfresco Repository folder content based on a Repository account configured in Alfresco Process Services (APS). | [Source](../lib/core/form/services/activiti-alfresco.service.ts) | +| [Form rendering service](form-rendering.service.md) | Maps an APS form field type string onto the corresponding form widget component type. | [Source](../lib/core/form/services/form-rendering.service.ts) | +| [Form service](form.service.md) | Implements Process Services form methods | [Source](../lib/core/form/services/form.service.ts) | +| [Node service](node.service.md) | Gets Alfresco Repository node metadata and creates nodes with metadata. | [Source](../lib/core/form/services/node.service.ts) | +| [Process content service](process-content.service.md) | Manipulates content related to a Process Instance or Task Instance in APS. | [Source](../lib/core/form/services/process-content.service.ts) | +| [Alfresco api service](alfresco-api.service.md) | Provides access to initialized **AlfrescoJSApi** instance. | [Source](../lib/core/services/alfresco-api.service.ts) | +| [Apps process service](apps-process.service.md) | Gets details of the Process Services apps that are deployed for the user. | [Source](../lib/core/services/apps-process.service.ts) | +| [Auth guard bpm service](auth-guard-bpm.service.md) | Adds authentication with Process Services to a route within the app. | [Source](../lib/core/services/auth-guard-bpm.service.ts) | +| [Auth guard ecm service](auth-guard-ecm.service.md) | Adds authentication with Content Services to a route within the app. | [Source](../lib/core/services/auth-guard-ecm.service.ts) | +| [Auth guard service](auth-guard.service.md) | Adds authentication to a route within the app. | [Source](../lib/core/services/auth-guard.service.ts) | +| [Authentication service](authentication.service.md) | Provides authentication for use with the Login component. | [Source](../lib/core/services/authentication.service.ts) | +| [Comment process service](comment-process.service.md) | Adds and retrieves comments for task and process instances in Process Services. | [Source](../lib/core/services/comment-process.service.ts) | +| [Content service](content.service.md) | Accesses app-generated data objects via URLs and file downloads. | [Source](../lib/core/services/content.service.ts) | +| [Cookie service](cookie.service.md) | Stores key-value data items as browser cookies. | [Source](../lib/core/services/cookie.service.ts) | +| [Deleted nodes api service](deleted-nodes-api.service.md) | Gets a list of Content Services nodes currently in the trash. | [Source](../lib/core/services/deleted-nodes-api.service.ts) | +| [Discovery api service](discovery-api.service.md) | Gets version and license information for Process Services and Content Services. | [Source](../lib/core/services/discovery-api.service.ts) | +| [Favorites api service](favorites-api.service.md) | Gets a list of items a user has marked as their favorites. | [Source](../lib/core/services/favorites-api.service.ts) | +| [Highlight transform service](highlight-transform.service.md) | Adds HTML to a string to highlight chosen sections. | [Source](../lib/core/services/highlight-transform.service.ts) | +| [Log service](log.service.md) | Provide a log functionality for your ADF application. | [Source](../lib/core/services/log.service.ts) | +| [Nodes api service](nodes-api.service.md) | Accesses and manipulates ACS document nodes using their node IDs. | [Source](../lib/core/services/nodes-api.service.ts) | +| [Notification service](notification.service.md) | Shows a notification message with optional feedback. | [Source](../lib/core/services/notification.service.ts) | +| [Page title service](page-title.service.md) | Sets the page title. | [Source](../lib/core/services/page-title.service.ts) | +| [People content service](people-content.service.md) | Gets information about a Content Services user. | [Source](../lib/core/services/people-content.service.ts) | +| [People process service](people-process.service.md) | Gets information about Process Services users. | [Source](../lib/core/services/people-process.service.ts) | +| [Renditions service](renditions.service.md) | Manages prearranged conversions of content to different formats. | [Source](../lib/core/services/renditions.service.ts) | +| [Search configuration service](search-configuration.service.md) | Provides fine control of parameters to a search. | [Source](../lib/core/services/search-configuration.service.ts) | +| [Shared links api service](shared-links-api.service.md) | Finds shared links to Content Services items. | [Source](../lib/core/services/shared-links-api.service.ts) | +| [Sites service](sites.service.md) | Accesses and manipulates sites from a Content Services repository. | [Source](../lib/core/services/sites.service.ts) | +| [Storage service](storage.service.md) | Stores items in the form of key-value pairs. | [Source](../lib/core/services/storage.service.ts) | +| [Thumbnail service](thumbnail.service.md) | Retrieves an SVG thumbnail image to represent a document type. | [Source](../lib/core/services/thumbnail.service.ts) | +| [Translation service](translation.service.md) | Supports localisation. | [Source](../lib/core/services/translation.service.ts) | +| [Upload service](upload.service.md) | Provides access to various APIs related to file upload features. | [Source](../lib/core/services/upload.service.ts) | +| [User preferences service](user-preferences.service.md) | Stores preferences for components. | [Source](../lib/core/services/user-preferences.service.ts) | +| [Bpm user service](bpm-user.service.md) | Gets information about the current Process Services user. | [Source](../lib/core/userinfo/services/bpm-user.service.ts) | +| [Ecm user service](ecm-user.service.md) | Gets information about a Content Services user. | [Source](../lib/core/userinfo/services/ecm-user.service.ts) | -- [Content widget](content.widget.md) - +## Widgets -### Other classes and interfaces +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Content widget](content.widget.md) | Shows the content preview. | [Source](../lib/core/form/components/widgets/content/content.widget.ts) | -- [DataTableAdapter interface](DataTableAdapter.md) -- [FormFieldValidator interface](FormFieldValidator.md) + + +## Other classes and interfaces + +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Data Table Adapter interface](DataTableAdapter.md) | Defines how table data is supplied to [DataTable](datatable.component.md) and [Tasklist](task-list.component.md) components. | [Source](../core/datatable/data/datatable-adapter.ts) | +| [Form Field Validator interface](FormFieldValidator.md) | Defines how the input fields of [ADF Form](form.component.md) and [ADF Task Details](task-details.component.md) components are validated. | [Source](../core/form/components/widgets/core/form-field-validator.ts) | +| [Search Configuration interface](search-configuration.interface.md) | Provides fine control of parameters to a search. | [Source](../core/services/search-configuration.service.ts) | [(Back to Contents)](#contents) @@ -146,52 +165,62 @@ Contains components related to Content Services. See the library's [README file](../lib/content-services/README.md) for more information about installing and using the source code. - -### Components + -- [Breadcrumb component](breadcrumb.component.md) -- [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) -- [Content node selector component](content-node-selector.component.md) -- [Content action component](content-action.component.md) -- [Document list component](document-list.component.md) -- [Search control component](search-control.component.md) -- [Search component](search.component.md) -- [Sites dropdown component](sites-dropdown.component.md) -- [Like component](like.component.md) -- [Rating component](rating.component.md) -- [Tag actions component](tag-actions.component.md) -- [Tag list component](tag-list.component.md) -- [Tag node list component](tag-node-list.component.md) -- [File uploading dialog component](file-uploading-dialog.component.md) -- [Upload button component](upload-button.component.md) -- [Upload drag area component](upload-drag-area.component.md) -- [Version list component](version-list.component.md) -- [Version manager component](version-manager.component.md) -- [Webscript component](webscript.component.md) -- [*Content metadata card component](../lib/content-services/content-metadata/content-metadata-card.component.ts) -- [*Content metadata component](../lib/content-services/content-metadata/content-metadata.component.ts) +## Components -### Directives +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Breadcrumb component](breadcrumb.component.md) | Indicates the current position within a navigation hierarchy. | [Source](../lib/content-services/breadcrumb/breadcrumb.component.ts) | +| [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) | Indicates the current position within a navigation hierarchy using a dropdown menu. | [Source](../lib/content-services/breadcrumb/dropdown-breadcrumb.component.ts) | +| [Content metadata component](content-metadata.component.md) | Allows a user to display and edit metadata related to a node. | [Source](../lib/content-services/content-metadata/components/content-metadata/content-metadata.component.ts) | +| [Content node selector panel component](content-node-selector-panel.component.md) | Opens a [Content Node Selector](content-node-selector.component.md) in its own dialog window. | [Source](../lib/content-services/content-node-selector/content-node-selector-panel.component.ts) | +| [Content node selector component](content-node-selector.component.md) | Allows a user to select items from a Content Services repository. | [Source](../lib/content-services/content-node-selector/content-node-selector.component.ts) | +| [Content action component](content-action.component.md) | Adds options to a Document List actions menu for a particular content type. | [Source](../lib/content-services/document-list/components/content-action/content-action.component.ts) | +| [Document list component](document-list.component.md) | Displays the documents from a repository. | [Source](../lib/content-services/document-list/components/document-list.component.ts) | +| [Search control component](search-control.component.md) | Displays a input text which shows find-as-you-type suggestions. | [Source](../lib/content-services/search/components/search-control.component.ts) | +| [Search component](search.component.md) | Searches items for supplied search terms. | [Source](../lib/content-services/search/components/search.component.ts) | +| [Sites dropdown component](sites-dropdown.component.md) | Displays a dropdown menu to show and interact with the sites of the current user. | [Source](../lib/content-services/site-dropdown/sites-dropdown.component.ts) | +| [Like component](like.component.md) | Allows a user to add "likes" to an item. | [Source](../lib/content-services/social/like.component.ts) | +| [Rating component](rating.component.md) | Allows a user to add ratings to an item. | [Source](../lib/content-services/social/rating.component.ts) | +| [Tag actions component](tag-actions.component.md) | Shows available actions for tags. | [Source](../lib/content-services/tag/tag-actions.component.ts) | +| [Tag list component](tag-list.component.md) | Shows tags for an item. | [Source](../lib/content-services/tag/tag-list.component.ts) | +| [Tag node list component](tag-node-list.component.md) | Shows tags for a node. | [Source](../lib/content-services/tag/tag-node-list.component.ts) | +| [File uploading dialog component](file-uploading-dialog.component.md) | Shows a dialog listing all the files uploaded with the Upload Button or Drag Area components. | [Source](../lib/content-services/upload/components/file-uploading-dialog.component.ts) | +| [Upload button component](upload-button.component.md) | Activates a file upload. | [Source](../lib/content-services/upload/components/upload-button.component.ts) | +| [Upload drag area component](upload-drag-area.component.md) | Adds a drag and drop area to upload files to Alfresco. | [Source](../lib/content-services/upload/components/upload-drag-area.component.ts) | +| [Version list component](version-list.component.md) | Displays the version history of a node in a [Version Manager component](version-manager.component.md) | [Source](../lib/content-services/version-manager/version-list.component.ts) | +| [Version manager component](version-manager.component.md) | Displays the version history of a node with the ability to upload a new version. | [Source](../lib/content-services/version-manager/version-manager.component.ts) | +| [Webscript component](webscript.component.md) | Provides access to Webscript features. | [Source](../lib/content-services/webscript/webscript.component.ts) | -- [Folder create directive](folder-create.directive.md) -- [Folder edit directive](folder-edit.directive.md) -- [File draggable directive](file-draggable.directive.md) +## Directives -### Models +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Folder create directive](folder-create.directive.md) | Allows folders to be created. | [Source](../lib/content-services/folder-directive/folder-create.directive.ts) | +| [Folder edit directive](folder-edit.directive.md) | Allows folders to be edited. | [Source](../lib/content-services/folder-directive/folder-edit.directive.ts) | +| [File draggable directive](file-draggable.directive.md) | Provide drag-and-drop features for an element such as a `div`. | [Source](../lib/content-services/upload/directives/file-draggable.directive.ts) | -- [Document library model](document-library.model.md) -- [Permissions style model](permissions-style.model.md) +## Models -### Services +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Document library model](document-library.model.md) | Defines classes for use with the Content Services node API. | [Source](../lib/content-services/document-list/models/document-library.model.ts) | +| [Permissions style model](permissions-style.model.md) | Sets custom CSS styles for rows of a [Document List](document-list.component.md) according to the item's permissions. | [Source](../lib/content-services/document-list/models/permissions-style.model.ts) | -- [Document actions service](document-actions.service.md) -- [Document list service](document-list.service.md) -- [Folder actions service](folder-actions.service.md) -- [Rating service](rating.service.md) -- [Tag service](tag.service.md) -- [*Content metadata service](../lib/content-services/content-metadata/content-metadata.service.ts) - +## Services + +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Content node dialog service](content-node-dialog.service.md) | Displays and manages dialogs for selecting content to open, copy or upload. | [Source](../lib/content-services/content-node-selector/content-node-dialog.service.ts) | +| [Document actions service](document-actions.service.md) | Implements the document menu actions for the Document List component. | [Source](../lib/content-services/document-list/services/document-actions.service.ts) | +| [Document list service](document-list.service.md) | Implements node operations used by the Document List component. | [Source](../lib/content-services/document-list/services/document-list.service.ts) | +| [Folder actions service](folder-actions.service.md) | Implements the folder menu actions for the Document List component. | [Source](../lib/content-services/document-list/services/folder-actions.service.ts) | +| [Rating service](rating.service.md) | Manages ratings for items in Content Services. | [Source](../lib/content-services/social/services/rating.service.ts) | +| [Tag service](tag.service.md) | Manages tags in Content Services. | [Source](../lib/content-services/tag/services/tag.service.ts) | + + [(Back to Contents)](#contents) @@ -201,52 +230,61 @@ Contains components related to Process Services. See the library's [README file](../lib/process-services/README.md) for more information about installing and using the source code. - -### Components + -- [Apps list component](apps-list.component.md) -- [Create process attachment component](create-process-attachment.component.md) -- [Create task attachment component](create-task-attachment.component.md) -- [Process attachment list component](process-attachment-list.component.md) -- [Task attachment list component](task-attachment-list.component.md) -- [Comment list component](comment-list.component.md) -- [Comments component](comments.component.md) -- [Process comments component](process-comments.component.md) -- [People list component](people-list.component.md) -- [People search component](people-search.component.md) -- [People component](people.component.md) -- [Process filters component](process-filters.component.md) -- [Process instance details component](process-instance-details.component.md) -- [Process instance header component](process-instance-header.component.md) -- [Process instance tasks component](process-instance-tasks.component.md) -- [Process list component](process-list.component.md) -- [Start process component](start-process.component.md) -- [Checklist component](checklist.component.md) -- [Start task component](start-task.component.md) -- [Task details component](task-details.component.md) -- [Task filters component](task-filters.component.md) -- [Task header component](task-header.component.md) -- [Task list component](task-list.component.md) +## Components -### Directives +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Apps list component](apps-list.component.md) | Shows all available apps. | [Source](../lib/process-services/app-list/apps-list.component.ts) | +| [Create process attachment component](create-process-attachment.component.md) | Displays Upload Component (Drag and Click) to upload the attachment to a specified process instance | [Source](../lib/process-services/attachment/create-process-attachment.component.ts) | +| [Create task attachment component](create-task-attachment.component.md) | Displays Upload Component (Drag and Click) to upload the attachment to a specified task | [Source](../lib/process-services/attachment/create-task-attachment.component.ts) | +| [Process attachment list component](process-attachment-list.component.md) | Displays attached documents on a specified process instance | [Source](../lib/process-services/attachment/process-attachment-list.component.ts) | +| [Task attachment list component](task-attachment-list.component.md) | Displays attached documents on a specified task. | [Source](../lib/process-services/attachment/task-attachment-list.component.ts) | +| [Comment list component](comment-list.component.md) | Shows a list of comments. | [Source](../lib/process-services/comments/comment-list.component.ts) | +| [Comments component](comments.component.md) | Displays comments from users involved in a specified task and allows an involved user to add a comment to the task. | [Source](../lib/process-services/comments/comments.component.ts) | +| [Process comments component](process-comments.component.md) | Displays comments associated with a particular process instance and allows the user to add new comments | [Source](../lib/process-services/comments/process-comments.component.ts) | +| [People list component](people-list.component.md) | Shows a list of users (people). | [Source](../lib/process-services/people/people-list.component.ts) | +| [People search component](people-search.component.md) | Searches users/people. | [Source](../lib/process-services/people/people-search.component.ts) | +| [People component](people.component.md) | Displays users involved with a specified task | [Source](../lib/process-services/people/people.component.ts) | +| [Process filters component](process-filters.component.md) | Collection of criteria used to filter process instances, which may be customized by users. | [Source](../lib/process-services/process-list/components/process-filters.component.ts) | +| [Process instance details component](process-instance-details.component.md) | Displays detailed information on a specified process instance | [Source](../lib/process-services/process-list/components/process-instance-details.component.ts) | +| [Process instance header component](process-instance-header.component.md) | Sub-component of the process details component, which renders some general information about the selected process. | [Source](../lib/process-services/process-list/components/process-instance-header.component.ts) | +| [Process instance tasks component](process-instance-tasks.component.md) | Lists both the active and completed tasks associated with a particular process instance | [Source](../lib/process-services/process-list/components/process-instance-tasks.component.ts) | +| [Process list component](process-list.component.md) | Renders a list containing all the process instances matched by the parameters specified. | [Source](../lib/process-services/process-list/components/process-list.component.ts) | +| [Start process component](start-process.component.md) | Starts a process. | [Source](../lib/process-services/process-list/components/start-process.component.ts) | +| [Checklist component](checklist.component.md) | Shows the checklist task functionality. | [Source](../lib/process-services/task-list/components/checklist.component.ts) | +| [Start task component](start-task.component.md) | Creates/Starts new task for the specified app | [Source](../lib/process-services/task-list/components/start-task.component.ts) | +| [Task details component](task-details.component.md) | Shows the details of the task id passed in input | [Source](../lib/process-services/task-list/components/task-details.component.ts) | +| [Task filters component](task-filters.component.md) | Shows all available filters. | [Source](../lib/process-services/task-list/components/task-filters.component.ts) | +| [Task header component](task-header.component.md) | Shows all the information related to a task. | [Source](../lib/process-services/task-list/components/task-header.component.ts) | +| [Task list component](task-list.component.md) | Renders a list containing all the tasks matched by the parameters specified. | [Source](../lib/process-services/task-list/components/task-list.component.ts) | -- [Process audit directive](process-audit.directive.md) -- [Task audit directive](task-audit.directive.md) +## Directives -### Models +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Process audit directive](process-audit.directive.md) | Fetches the Process Audit information in the pdf or json format. | [Source](../lib/process-services/process-list/components/process-audit.directive.ts) | +| [Task audit directive](task-audit.directive.md) | Fetches the Task Audit information in the pdf or json format. | [Source](../lib/process-services/task-list/components/task-audit.directive.ts) | -- [Filter model](filter.model.md) -- [Task details model](task-details.model.md) +## Models -### Services +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Filter model](filter.model.md) | Contains classes related to filters in Process Services. | [Source](../lib/process-services/task-list/models/filter.model.ts) | +| [Task details model](task-details.model.md) | Information about a task. | [Source](../lib/process-services/task-list/models/task-details.model.ts) | -- [Process filter service](process-filter.service.md) -- [Process service](process.service.md) -- [Task filter service](task-filter.service.md) -- [Tasklist service](tasklist.service.md) -- [*Task upload service](../lib/process-services/task-list/services/task-upload.service.ts) - +## Services + +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Process filter service](process-filter.service.md) | Manage Process Filters, which are pre-configured Process Instance queries. | [Source](../lib/process-services/process-list/services/process-filter.service.ts) | +| [Process service](process.service.md) | Manage Process Instances, Process Variables, and Process Audit Log. | [Source](../lib/process-services/process-list/services/process.service.ts) | +| [Task filter service](task-filter.service.md) | Manage Task Filters, which are pre-configured Task Instance queries. | [Source](../lib/process-services/task-list/services/task-filter.service.ts) | +| [Tasklist service](tasklist.service.md) | Manage Task Instances. | [Source](../lib/process-services/task-list/services/tasklist.service.ts) | + + [(Back to Contents)](#contents) @@ -256,15 +294,19 @@ Contains components for Process Services analytics and diagrams. See the library's [README file](../lib/insights/README.md) for more information about installing and using the source code. - -### Components + -- [Analytics generator component](analytics-generator.component.md) -- [Analytics report list component](analytics-report-list.component.md) -- [Analytics component](analytics.component.md) -- [Widget component](widget.component.md) -- [Diagram component](diagram.component.md) - +## Components -[(Back to Contents)](#contents) \ No newline at end of file +| Name | Description | Source link | +| ---- | ----------- | ----------- | +| [Widget component](widget.component.md) | Base class for standard and custom widget classes. | [Source](../lib/insights/analytics-process/components/widgets/widget.component.ts) | +| [Analytics generator component](analytics-generator.component.md) | Generates and shows charts | [Source](../lib/insights/analytics-process/components/analytics-generator.component.ts) | +| [Analytics report list component](analytics-report-list.component.md) | Shows a list of all available reports | [Source](../lib/insights/analytics-process/components/analytics-report-list.component.ts) | +| [Analytics component](analytics.component.md) | Shows the charts related to the reportId passed as input | [Source](../lib/insights/analytics-process/components/analytics.component.ts) | +| [Diagram component](diagram.component.md) | This component shows the diagram of a process. | [Source](../lib/insights/diagram/components/diagram.component.ts) | + + + +[(Back to Contents)](#contents) diff --git a/docs/accordion-group.component.md b/docs/accordion-group.component.md index 9def0e6bef..d329b66704 100644 --- a/docs/accordion-group.component.md +++ b/docs/accordion-group.component.md @@ -32,20 +32,24 @@ export class MyComponent implements OnInit { ### Properties | Name | Type | Description | -| --- | --- | --- | -| heading | string | Title heading for the group. | -| isSelected | boolean | Is this group currently selected? | -| headingIcon | string | The material design icon. | -| hasAccordionIcon | boolean | Should the (expanded) accordion icon be shown? Defaults to true | -| headingIconTooltip | string | Tooltip message to be shown for headingIcon | +| ---- | ---- | ----------- | +| heading | `string` | Title heading for the group. | +| headingIcon | `string` | The material design icon. | +| headingIconTooltip | `string` | Tooltip message to be shown for headingIcon | +| hasAccordionIcon | `boolean` | Should the (expanded) accordion icon be shown?
Default value: `true` | +| isOpen | `boolean` | Is this group currently open? | +| isSelected | `boolean` | Is this group currently selected? | + +### Events + +| Name | Type | Description | +| ---- | ---- | ----------- | +| headingClick | `EventEmitter` | Emitted when the heading is clicked. | ## Details Place one or more accordion groups within an [Accordion component](accordion.component.md) to define a menu. - - ## See also -- [Accordion component](accordion.component.md) - \ No newline at end of file +- [Accordion component](accordion.component.md) diff --git a/docs/accordion.component.md b/docs/accordion.component.md index c7c1867298..cc22c96aac 100644 --- a/docs/accordion.component.md +++ b/docs/accordion.component.md @@ -29,15 +29,6 @@ export class MyComponent implements OnInit { } ``` -### Properties - -| Name | Type | Description | -| --- | --- | --- | -| heading | string | The header title. | -| isSelected | boolean | Define if the accordion group is selected or not. | -| headingIcon | string | The material design icon. | -| hasAccordionIcon | boolean | Define if the accordion (expand) icon needs to be shown or not, the default value is true | - ## Details An accordion menu contains several panels of content, only one of which is visible at any time. The @@ -69,9 +60,7 @@ the following example: ![how-create-accordion-menu](docassets/images/how-to-create-accordion-menu.png) - - ## See also -- [Accordion group component](accordion-group.component.md) - \ No newline at end of file +- [Accordion group component](accordion-group.component.md) + diff --git a/docs/activiti-alfresco.service.md b/docs/activiti-alfresco.service.md index cc14a55758..d90b47d8ee 100644 --- a/docs/activiti-alfresco.service.md +++ b/docs/activiti-alfresco.service.md @@ -1,6 +1,7 @@ # Activiti Alfresco Content Service -Get Alfresco Repository folder content based on a Repository account configured in Alfresco Process Services (APS). +Gets Alfresco Repository folder content based on a Repository account configured in Alfresco Process Services (APS). + It is possible to configure multiple Alfresco Repository accounts in APS (i.e. multiple Alfresco Servers). This service can also be used to link Alfresco content as related content in APS. Content such as documents and other files can be attached to Process Instances diff --git a/docs/alfresco-api.service.md b/docs/alfresco-api.service.md index 3101bfd562..e7682c9488 100644 --- a/docs/alfresco-api.service.md +++ b/docs/alfresco-api.service.md @@ -2,17 +2,6 @@ Provides access to initialized **AlfrescoJSApi** instance. - - - - -- [Basic Usage](#basic-usage) -- [Details](#details) - - - - - ## Basic Usage ```ts @@ -42,4 +31,4 @@ Alfresco JS api by casting the instance to `any` type like the following:_ ```ts let api: any = this.apiService.getInstance(); api.nodes.addNode('-root-', body, {}); -``` \ No newline at end of file +``` diff --git a/docs/analytics-generator.component.md b/docs/analytics-generator.component.md index 4d5c77fc61..3071fc7015 100644 --- a/docs/analytics-generator.component.md +++ b/docs/analytics-generator.component.md @@ -14,20 +14,13 @@ Generates and shows charts ### Properties | Name | Type | Description | -| --- | --- | -- | +| ---- | ---- | ----------- | | reportId | string | The report id | | reportParamQuery | ReportQuery | The object contains all the parameters that the report needs | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | Raised when the charts are loaded | | error | Raised when an error occurs during the loading | - - - -## See also - - - \ No newline at end of file diff --git a/docs/analytics-report-list.component.md b/docs/analytics-report-list.component.md index a30a948984..4139a9e89f 100644 --- a/docs/analytics-report-list.component.md +++ b/docs/analytics-report-list.component.md @@ -13,22 +13,15 @@ Shows a list of all available reports ### Properties | Name | Type | Required | Default | Description | -| --- | --- | --- | --- | --- | -| appId | string | optional | | The application id | -| layoutType | string | required | | Define the layout of the apps. There are two possible values: GRID or LIST. LIST is the default value| -| selectFirst | boolean | optional | false | Change the value to true if you want to select the first item in the list as default| +| ---- | ---- | -------- | ------- | ----------- | +| appId | string | optional | | The application id | +| layoutType | string | required | | Define the layout of the apps. There are two possible values: GRID or LIST. LIST is the default value | +| selectFirst | boolean | optional | false | Change the value to true if you want to select the first item in the list as default | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | The event is emitted when the report list is loaded | | error | The event is emitted when an error occurs during the loading | | reportClick | The event is emitted when the report in the list is selected | - - - -## See also - - - \ No newline at end of file diff --git a/docs/analytics.component.md b/docs/analytics.component.md index d3869435eb..da927e3861 100644 --- a/docs/analytics.component.md +++ b/docs/analytics.component.md @@ -16,7 +16,7 @@ Shows the charts related to the reportId passed as input ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | appId | string | The application id | | reportId | string | The report id | | hideParameters | boolean | Toggle the analytics parameters | @@ -36,15 +36,8 @@ You can also hide chart parameters UI by setting the `hideParameters` to `true`: ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | The event is emitted when the report parameters are loaded | | error | The event is emitted when an error occurs during the loading | | reportSaved | The event is emitted when a report is saved | | reportDeleted | The event is emitted when a report is deleted | - - - -## See also - - - \ No newline at end of file diff --git a/docs/angular-material-design.md b/docs/angular-material-design.md index b5fd0dece5..b1ec5cb69f 100644 --- a/docs/angular-material-design.md +++ b/docs/angular-material-design.md @@ -2,7 +2,7 @@ Google's [Material Design](https://material.io/guidelines/material-design/introduction.html) -is an example of a *design language*, a +is an example of a _design language_, a general set of principles and ideas that are used to give designs a consistent look and feel. A design language might be used in-house by a company to maintain a "family resemblance" between its products or to @@ -53,9 +53,6 @@ See the Material Design [system icon docs](https://material.io/guidelines/style/icons.html#icons-system-icons) for further information and to download the set of standard icon images. - - ## See also -- [Theming](theming.md) - \ No newline at end of file +- [Theming](theming.md) diff --git a/docs/app-config.service.md b/docs/app-config.service.md index 70f2340be6..64e265599f 100644 --- a/docs/app-config.service.md +++ b/docs/app-config.service.md @@ -2,17 +2,6 @@ Supports app configuration settings, stored server side. - - - - -- [Details](#details) - * [Variable substitution in configuration strings](#variable-substitution-in-configuration-strings) - - - - - ## Details The `AppConfigService` service provides support for loading and accessing global application configuration settings that you store on the server side in the form of a JSON file. @@ -102,6 +91,6 @@ The `AppConfigService` also supports a limited set of variable substitutions to The supported variables are: | Variable name | Runtime value | -| --- | --- | +| ------------- | ------------- | | hostname | `location.hostname` | | port | `location.port` | diff --git a/docs/apps-list.component.md b/docs/apps-list.component.md index 2f4640fcd5..9d9e3d3c51 100644 --- a/docs/apps-list.component.md +++ b/docs/apps-list.component.md @@ -1,4 +1,4 @@ -# Activiti Apps Component +# Apps List Component Shows all available apps. @@ -15,15 +15,16 @@ Shows all available apps. ### Properties | Name | Type | Description | -| --- | --- | --- | -| layoutType | string | (**required**) Define the layout of the apps. There are two possible values: GRID or LIST. | -| filtersAppId | Object | Provide a way to filter the apps to show. | +| ---- | ---- | ----------- | +| layoutType | `string` | (\*\*required\*\*) Defines the layout of the apps. There are two possible values, "GRID" and "LIST".
Default value: `AppsListComponent.LAYOUT_GRID` | +| filtersAppId | `any[]` | Provides a way to filter the apps to show. | ### Events -| Name | Description | -| --- | --- | -| appClick | Raised when an app entry is clicked | +| Name | Type | Description | +| ---- | ---- | ----------- | +| appClick | `EventEmitter` | Emitted when an app entry is clicked. | +| error | `EventEmitter` | Emitted when an error occurs. | ## Details diff --git a/docs/auth-guard-bpm.service.md b/docs/auth-guard-bpm.service.md index e60e94cc6c..fbba383834 100644 --- a/docs/auth-guard-bpm.service.md +++ b/docs/auth-guard-bpm.service.md @@ -31,13 +31,7 @@ either ACS or APS. See the [ADF custom page tutorial](https://community.alfresco.com/docs/DOC-6628-adf-105-creating-custom-pages-and-components) for worked examples of all three guards. - - ## See also -- [Auth guard ecm service](auth-guard-ecm.service.md) -- [Auth guard service](auth-guard.service.md) - - - - +- [Auth guard ecm service](auth-guard-ecm.service.md) +- [Auth guard service](auth-guard.service.md) diff --git a/docs/auth-guard-ecm.service.md b/docs/auth-guard-ecm.service.md index 213867218e..b19dcb2bff 100644 --- a/docs/auth-guard-ecm.service.md +++ b/docs/auth-guard-ecm.service.md @@ -31,13 +31,7 @@ either APS or ACS. See the [ADF custom page tutorial](https://community.alfresco.com/docs/DOC-6628-adf-105-creating-custom-pages-and-components) for worked examples of all three guards. - - ## See also -- [Auth guard service](auth-guard.service.md) -- [Auth guard bpm service](auth-guard-bpm.service.md) - - - - +- [Auth guard service](auth-guard.service.md) +- [Auth guard bpm service](auth-guard-bpm.service.md) diff --git a/docs/auth-guard.service.md b/docs/auth-guard.service.md index ea6a49a8e2..dd2ea4cb8d 100644 --- a/docs/auth-guard.service.md +++ b/docs/auth-guard.service.md @@ -32,13 +32,7 @@ against APS or ACS, respectively. See the [ADF custom page tutorial](https://community.alfresco.com/docs/DOC-6628-adf-105-creating-custom-pages-and-components) for worked examples of all three guards. - - ## See also -- [Auth guard bpm service](auth-guard-bpm.service.md) -- [Auth guard ecm service](auth-guard-ecm.service.md) - - - - +- [Auth guard bpm service](auth-guard-bpm.service.md) +- [Auth guard ecm service](auth-guard-ecm.service.md) diff --git a/docs/authentication.service.md b/docs/authentication.service.md index 395695cd1d..8c81d3f59b 100644 --- a/docs/authentication.service.md +++ b/docs/authentication.service.md @@ -2,18 +2,6 @@ Provides authentication for use with the Login component. - - - - -- [Basic Usage](#basic-usage) - * [Events](#events) -- [Details](#details) - - - - - ## Basic Usage **app.component.ts** @@ -39,10 +27,10 @@ export class AppComponent { ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | onLogin | Raised when user logs in | | onLogout | Raised when user logs out | ## Details -The authentication service is used inside the [login component](../ng2-components/ng2-alfresco-login/README.md) and is possible to find there an example of how to use it. +The authentication service is used inside the [login component](login.component.md) and is possible to find there an example of how to use it. diff --git a/docs/bpm-user.model.md b/docs/bpm-user.model.md index 03de2832b7..feba39a405 100644 --- a/docs/bpm-user.model.md +++ b/docs/bpm-user.model.md @@ -14,11 +14,8 @@ it is likely to change and so its properties are not listed here. For the latest version of the class, see the [source file](https://github.com/Alfresco/alfresco-ng2-components/blob/development/lib/core/userinfo/models/bpm-user.model.ts). - - ## See also -- [Bpm user service](bpm-user.service.md) -- [Ecm user model](ecm-user.model.md) -- [People process service](people-process.service.md) - \ No newline at end of file +- [Bpm user service](bpm-user.service.md) +- [Ecm user model](ecm-user.model.md) +- [People process service](people-process.service.md) diff --git a/docs/bpm-user.service.md b/docs/bpm-user.service.md index d5d551f6fd..73c08c8fb5 100644 --- a/docs/bpm-user.service.md +++ b/docs/bpm-user.service.md @@ -21,10 +21,7 @@ and [getProfilePictureUrl](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-activiti-rest-api/docs/ProfileApi.md#getProfilePictureUrl) methods in the Alfresco JS API for more information about the REST calls used by this service. - - ## See also -- [Ecm user service](ecm-user.service.md) -- [Bpm user model](bpm-user.model.md) - \ No newline at end of file +- [Ecm user service](ecm-user.service.md) +- [Bpm user model](bpm-user.model.md) diff --git a/docs/breadcrumb.component.md b/docs/breadcrumb.component.md index e7a3f548b5..ca632764b3 100644 --- a/docs/breadcrumb.component.md +++ b/docs/breadcrumb.component.md @@ -16,22 +16,19 @@ Indicates the current position within a navigation hierarchy. ### Properties | Name | Type | Description | -| --- | --- | --- | -| target | DocumentListComponent | (optional) DocumentList component to operate with. Upon clicks will instruct the given component to update. | -| folderNode | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md) | Active node, builds UI based on `folderNode.path.elements` collection. | -| root | string | (optional) Name of the root element of the breadcrumb. You can use this property to rename "Company Home" to "Personal Files" for example. You can use i18n resource key for the property value. | -| rootId | string | (optional) The id of the root element. You can use this property to set a custom element the breadcrumb should start with. | +| ---- | ---- | ----------- | +| folderNode | `MinimalNodeEntryEntity` | Active node, builds UI based on folderNode.path.elements collection.
Default value: `null` | +| root | `string` | (optional) Name of the root element of the breadcrumb. You can use this property to rename "Company Home" to "Personal Files" for example. You can use an i18n resource key for the property value.
Default value: `null` | +| rootId | `string` | (optional) The id of the root element. You can use this property to set a custom element the breadcrumb should start with.
Default value: `null` | +| target | `DocumentListComponent` | (optional) Document List component to operate with. The list will update when the breadcrumb is clicked. | ### Events -| Name | Returned Type | Description | -| --- | --- | --- | -| navigate | [PathElementEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/PathElementEntity.md) | emitted when user clicks on a breadcrumb | +| Name | Type | Description | +| ---- | ---- | ----------- | +| navigate | `EventEmitter` | Emitted when the user clicks on a breadcrumb. | - - ## See also -- [Document list component](document-list.component.md) -- [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) - \ No newline at end of file +- [Document list component](document-list.component.md) +- [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) diff --git a/docs/card-view.component.md b/docs/card-view.component.md index 620c0629ac..c333533cc3 100644 --- a/docs/card-view.component.md +++ b/docs/card-view.component.md @@ -8,19 +8,19 @@ Displays a configurable property list renderer. -- [Basic Usage](#basic-usage) - * [Properties](#properties) -- [Details](#details) - * [Editing](#editing) - * [Defining properties](#defining-properties) - + [Card Text Item](#card-text-item) - + [Card Map Item](#card-map-item) - + [Card Date Item](#card-date-item) - * [Defining your custom card Item](#defining-your-custom-card-item) - + [1. Define the model for the custom type](#1-define-the-model-for-the-custom-type) - + [2. Define the component for the custom type](#2-define-the-component-for-the-custom-type) - + [3. Add you custom component to your module's entryComponents list](#3-add-you-custom-component-to-your-modules-entrycomponents-list) -- [See also](#see-also) +- [Basic Usage](#basic-usage) + - [Properties](#properties) +- [Details](#details) + - [Editing](#editing) + - [Defining properties](#defining-properties) + - [Card Text Item](#card-text-item) + - [Card Map Item](#card-map-item) + - [Card Date Item](#card-date-item) + - [Defining your custom card Item](#defining-your-custom-card-item) + - [1. Define the model for the custom type](#1-define-the-model-for-the-custom-type) + - [2. Define the component for the custom type](#2-define-the-component-for-the-custom-type) + - [3. Add you custom component to your module's entryComponents list](#3-add-you-custom-component-to-your-modules-entrycomponents-list) +- [See also](#see-also) @@ -33,15 +33,15 @@ Displays a configurable property list renderer. [properties]="[{label: 'My Label', value: 'My value'}]" [editable]="false"> - ``` ### Properties -| Name | Type | Description | -| --- | --- | --- | -| properties | [CardViewItem](#cardviewitem)[] | (**required**) The custom view to render | -| editable | boolean | If the component editable or not | +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| properties | [CardViewItem](#defining-properties)\[] | - | (**required**) The custom view to render | +| editable | boolean | - | If the component editable or not | +| displayEmpty | boolean | true | Whether to show empty properties in non-editable mode | ## Details @@ -50,8 +50,9 @@ You define the property list, the CardViewComponent does the rest. Each property ### Editing The card view can optionally allow its properties to be edited. You can control the editing of the properties in two level. -- **global level** - *via the editable parameter of the card-view.component* -- **property level** - *in each property via the editable attribute* + +- **global level** - _via the editable parameter of the card-view.component_ +- **property level** - _in each property via the editable attribute_ If you set the global editable parameter to false, no properties can be edited regardless of what is set inside the property. @@ -75,11 +76,15 @@ export interface CardViewItem { } ``` -At the moment three models are defined out of the box: +At the moment these are the models supported: -- **CardViewTextItemModel** - *for text items* -- **CardViewMapItemModel** - *for map items* -- **CardViewDateItemModel** - *for date items* +- **CardViewTextItemModel** - _for text items_ +- **CardViewMapItemModel** - _for map items_ +- **CardViewDateItemModel** - _for date items_ +- **CardViewDatetimeItemModel** - _for datetime items_ +- **CardViewBoolItemModel** - _for bool items (checkbox)_ +- **CardViewIntItemModel** - _for integers items_ +- **CardViewFloatItemModel** - _for float items_ Each of them extends the abstract CardViewBaseItemModel class to add some custom functionality to the basic behaviour. @@ -100,13 +105,39 @@ Each of them extends the abstract CardViewBaseItemModel class to add some custom clickable: true }), new CardViewDateItemModel({ - label: 'Birth of date', + label: 'Date of birth', value: someDate, - key: 'birth-of-date', + key: 'date-of-birth', default: new Date(), format: '', editable: true }), + new CardViewDatetimeItemModel({ + label: 'Datetime of birth', + value: someDatetime, + key: 'datetime-of-birth', + default: new Date(), + format: '', + editable: true + }), + new CardViewBoolItemModel({ + label: 'Vulcanian', + value: true, + key: 'vulcanian', + default: false + }), + new CardViewIntItemModel({ + label: 'Intelligence', + value: 213, + key: 'intelligence', + default: 1 + }), + new CardViewFloatItemModel({ + label: 'Mental stability', + value: 9.9, + key: 'mental-stability', + default: 0.0 + }), ... ] ``` @@ -120,16 +151,16 @@ const textItemProperty = new CardViewTextItemModel(options); ``` | Name | Type | Default | Description | -| --- | --- | --- | --- | -| label* | string | --- | The label to render | -| value* | any | --- | The original value | -| key* | string | --- | the key of the property. Have an important role when editing the property. | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | any | --- | The original value | +| key\* | string | --- | the key of the property. Have an important role when editing the property. | | default | any | --- | The default value to render in case the value is empty | -| displayValue* | string | --- | The value to render | +| displayValue\* | string | --- | The value to render | | editable | boolean | false | Whether the property editable or not | | clickable | boolean | false | Whether the property clickable or not | | multiline | string | false | Single or multiline text | -| pipes | CardViewTextItemPipeProperty[] | [] | Pipes to be applied on the displayValue | +| pipes | CardViewTextItemPipeProperty\[] | \[] | Pipes to be applied on the displayValue | ##### Using pipes in Card Text Item @@ -159,12 +190,12 @@ const mapItemProperty = new CardViewMapItemModel(options); ``` | Name | Type | Default | Description | -| --- | --- | --- | --- | -| label* | string | --- | The label to render | -| value* | Map | --- | A map that contains the key value pairs | -| key* | string | --- | the key of the property. Have an important role when editing the property. | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | Map | --- | A map that contains the key value pairs | +| key\* | string | --- | the key of the property. | | default | any | --- | The default value to render in case the value is empty | -| displayValue* | string | --- | The value to render | +| displayValue\* | string | --- | The value to render | | clickable | boolean | false | Whether the property clickable or not | #### Card Date Item @@ -176,15 +207,84 @@ const dateItemProperty = new CardViewDateItemModel(options); ``` | Name | Type | Default | Description | -| --- | --- | --- | --- | -| label* | string | --- | The label to render | -| value* | any | --- | The original value | -| key* | string | --- | the key of the property. Have an important role when editing the property. | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | any | --- | The original value | +| key\* | string | --- | the key of the property. | | default | any | --- | The default value to render in case the value is empty | -| displayValue* | string | --- | The value to render | +| displayValue\* | any | --- | The value to render | | editable | boolean | false | Whether the property editable or not | | format | boolean | "MMM DD YYYY" | any format that momentjs accepts | +#### Card Datetime Item + +CardViewDatetimeItemModel is a property type for datetime properties. + +```js +const datetimeItemProperty = new CardViewDatetimeItemModel(options); +``` + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | any | --- | The original value | +| key\* | string | --- | the key of the property. | +| default | any | any | The default value to render in case the value is empty | +| displayValue\* | string | --- | The value to render | +| editable | boolean | false | Whether the property editable or not | +| format | boolean | "MMM DD YYYY HH:mm" | any format that momentjs accepts | + +#### Card Bool Item + +CardViewBoolItemModel is a property type for boolean properties. + +```js +const boolItemProperty = new CardViewBoolItemModel(options); +``` + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | boolean | --- | The original value | +| key\* | string | --- | the key of the property. | +| default | boolean | false | The default value to render in case the value is empty | +| displayValue\* | boolean | --- | The value to render | +| editable | boolean | false | Whether the property editable or not | + +#### Card Int Item + +CardViewIntItemModel is a property type for integer properties. + +```js +const intItemProperty = new CardViewIntItemModel(options); +``` + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | integer | --- | The original value | +| key\* | string | --- | the key of the property. | +| default | integer | --- | The default value to render in case the value is empty | +| displayValue\* | integer | --- | The value to render | +| editable | boolean | false | Whether the property editable or not | + +#### Card Float Item + +CardViewFloatItemModel is a property type for float properties. + +```js +const floatItemProperty = new CardViewFloatItemModel(options); +``` + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| label\* | string | --- | The label to render | +| value\* | float | --- | The original value | +| key\* | string | --- | the key of the property. | +| default | float | --- | The default value to render in case the value is empty | +| displayValue\* | float | --- | The value to render | +| editable | boolean | false | Whether the property editable or not | + ### Defining your custom card Item Card item components are loaded dynamically, which makes you able to define your own custom component for the custom card item type. @@ -194,7 +294,7 @@ Let's consider you want to have a **stardate** type to display Captain Picard's #### 1. Define the Model for the custom type Your model has to extend the **CardViewBaseItemModel** and implement the **CardViewItem** and **DynamicComponentModel** interface. -*(You can check how the CardViewTextItemModel is implemented for further guidance.)* +_(You can check how the CardViewTextItemModel is implemented for further guidance.)_ ```js import { CardViewBaseItemModel, CardViewItem, DynamicComponentModel } from '@alfresco/adf-core'; @@ -231,7 +331,7 @@ export class CardViewStarDateItemComponent { constructor(private cardViewUpdateService: CardViewUpdateService) {} - isEditble() { + isEditable() { return this.editable && this.property.editable; } @@ -239,8 +339,8 @@ export class CardViewStarDateItemComponent { ... } } - ``` + To make your component editable, you can have a look on either the CardViewTextItemComponent' or on the CardViewDateItemComponent's source. #### 3. Add you custom component to your module's entryComponents list @@ -283,8 +383,10 @@ export class SomeParentComponent { ``` + + ## See also -- [Card view update service](card-view-update.service.md) - \ No newline at end of file +- [Card view update service](card-view-update.service.md) + diff --git a/docs/checklist.component.md b/docs/checklist.component.md index 65c76cba67..4528ec4f4a 100644 --- a/docs/checklist.component.md +++ b/docs/checklist.component.md @@ -1,4 +1,4 @@ -# Activiti Checklist Component +# Checklist Component Shows the checklist task functionality. @@ -15,7 +15,15 @@ Shows the checklist task functionality. ### Properties | Name | Type | Description | -| --- | --- | --- | -| taskId | string | (**required**) The id of the parent task which sub tasks are attached on. | -| readOnlyForm | boolean | Toggle readonly state of the form. Enforces all form widgets render readonly if enabled. | -| assignee | string | (**required**) The assignee id where the subtasks are assigned to. | +| ---- | ---- | ----------- | +| taskId | `string` | (required) The id of the parent task to which subtasks are attached. | +| readOnly | `boolean` | Toggle readonly state of the form. All form widgets will render as readonly if enabled.
Default value: `false` | +| assignee | `string` | (required) The assignee id that the subtasks are assigned to. | + +### Events + +| Name | Type | Description | +| ---- | ---- | ----------- | +| checklistTaskCreated | `EventEmitter` | Emitted when a new checklist task is created. | +| checklistTaskDeleted | `EventEmitter` | Emitted when a checklist task is deleted. | +| error | `EventEmitter` | Emitted when an error occurs. | diff --git a/docs/comment-list.component.md b/docs/comment-list.component.md index 3d164d1d82..d90a9ec9ae 100644 --- a/docs/comment-list.component.md +++ b/docs/comment-list.component.md @@ -1,9 +1,11 @@ # Comment list component + Shows a list of comments. ![ADF Comment List](docassets/images/adf-comment-list.png) ## Basic Usage + Populate the comments in the component class: ```ts @@ -39,7 +41,6 @@ export class SomeComponent implements OnInit { onClickCommentRow(comment: CommentProcessModel) { console.log('Clicked row: ', comment); } - ``` In the component template use the comment list component: @@ -53,17 +54,12 @@ In the component template use the comment list component: ### Properties -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| comments | CommentProcessModel[] | | The comments data used to populate the list | +| Name | Type | Description | +| ---- | ---- | ----------- | +| comments | `any[]` | The comments data used to populate the list. | ### Events -| Name | Description | -| --- | --- | -| clickRow | Emitted when the user clicks on one of the comment rows | - - - - - \ No newline at end of file +| Name | Type | Description | +| ---- | ---- | ----------- | +| clickRow | `EventEmitter` | Emitted when the user clicks on one of the comment rows. | diff --git a/docs/comment-process.model.md b/docs/comment-process.model.md index d24f45bb5f..96d8fe6e15 100644 --- a/docs/comment-process.model.md +++ b/docs/comment-process.model.md @@ -19,9 +19,6 @@ class CommentProcessModel implements CommentRepresentation { } ``` - - ## See also -- [Comment process service](comment-process.service.md) - \ No newline at end of file +- [Comment process service](comment-process.service.md) diff --git a/docs/comment-process.service.md b/docs/comment-process.service.md index 4a8f479293..7e0f258d68 100644 --- a/docs/comment-process.service.md +++ b/docs/comment-process.service.md @@ -23,12 +23,6 @@ comments returned by the methods of this service. Also, the Comments API section [Alfresco JS API docs](https://github.com/Alfresco/alfresco-js-api/tree/master/src/alfresco-activiti-rest-api) has further details about the underlying REST API. - - ## See also -- [Comment process model](comment-process.model.md) - - - - +- [Comment process model](comment-process.model.md) diff --git a/docs/comments.component.md b/docs/comments.component.md index 640da34a1b..818aff2d2e 100644 --- a/docs/comments.component.md +++ b/docs/comments.component.md @@ -1,4 +1,4 @@ -# ADF Comments Component +# Comments Component Displays comments from users involved in a specified task and allows an involved user to add a comment to the task. @@ -16,12 +16,12 @@ Displays comments from users involved in a specified task and allows an involved ### Properties | Name | Type | Description | -| --- | --- | --- | -| taskId | string | The numeric ID of the task | -| readOnly | boolean | The boolean flag | +| ---- | ---- | ----------- | +| taskId | `string` | The numeric ID of the task. | +| readOnly | `boolean` | Are the comments read only?
Default value: `false` | ### Events -| Name | Description | -| --- | --- | -| error | Raised when an error occurs while displaying/adding a comment | +| Name | Type | Description | +| ---- | ---- | ----------- | +| error | `EventEmitter` | Emitted when an error occurs while displaying/adding a comment. | diff --git a/docs/content-action.component.md b/docs/content-action.component.md index 10576cb19d..12e02e12a7 100644 --- a/docs/content-action.component.md +++ b/docs/content-action.component.md @@ -4,30 +4,25 @@ Adds options to a Document List actions menu for a particular content type. ![Document Actions](docassets/images/document-actions.png) - +## Contents - +- [Basic Usage](#basic-usage) -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - * [Built-in action examples](#built-in-action-examples) - + [Delete - System handler combined with custom handler](#delete---system-handler-combined-with-custom-handler) - + [Download](#download) - + [Copy and move](#copy-and-move) - * [Error, Permission and Success callbacks](#error-permission-and-success-callbacks) - * [Customizing built-in actions](#customizing-built-in-actions) -- [See also](#see-also) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [Built-in action examples](#built-in-action-examples) + - [Error, Permission and Success callbacks](#error-permission-and-success-callbacks) + - [Customizing built-in actions](#customizing-built-in-actions) + +- [See also](#see-also) ## Basic Usage ```html - + @@ -79,24 +74,24 @@ export class MyView { ### Properties -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| `target` | string | | "document" or "folder" | -| `title` | string | | The title of the action as shown in the menu | -| `icon` | string | | The name of the icon to display next to the menu command (can be left blank) | -| `handler` | string | | System type actions. Can be "delete", "download", "copy" or "move" | -| `permission` | string | | The name of the permission | -| `disabled` | boolean | | Is the menu item disabled? -| `disableWithNoPermission` | boolean | | Should this action be disabled in the menu if the user doesn't have permission for it? | +| Name | Type | Description | +| ---- | ---- | ----------- | +| title | `string` | The title of the action as shown in the menu.
Default value: `'Action'` | +| icon | `string` | The name of the icon to display next to the menu command (can be left blank). | +| handler | `string` | System actions. Can be "delete", "download", "copy" or "move". | +| target | `string` | Type of item that the action appies to. Can be "document" or "folder" | +| permission | `string` | The permission type. | +| disableWithNoPermission | `boolean` | Should this action be disabled in the menu if the user doesn't have permission for it? | +| disabled | `boolean` | Is the menu item disabled?
Default value: `false` | ### Events -| Name | Handler | Description | -| --- | --- | --- | -| `execute` | All | Emitted when user clicks on the action. For combined handlers see below | -| `permissionEvent` | All | Emitted when a permission error happens | -| `success` | copy, move, delete | Emitted on successful action with the success string message | -| `error` | copy, move | Emitted on unsuccessful action with the error event | +| Name | Type | Description | +| ---- | ---- | ----------- | +| execute | `EventEmitter<{}>` | Emitted when the user selects the action from the menu. | +| permissionEvent | `EventEmitter<{}>` | Emitted when a permission error occurs | +| error | `EventEmitter<{}>` | Emitted when an error occurs during the action. Applies to copy and move actions. | +| success | `EventEmitter<{}>` | Emitted when the action succeeds with the success string message. Applies to copy, move and delete actions. | ## Details @@ -105,16 +100,53 @@ The document actions are rendered on a dropdown menu for each items of content. A number of built-in actions are defined to handle common use cases: -- **Download** (document) -- **Copy** (document, folder) -- **Move** (document, folder) -- **Delete** (document, folder) +- **Download** (document) +- **Copy** (document, folder) +- **Move** (document, folder) +- **Delete** (document, folder) You can use one of the built-in handlers by assigning its name to the `handler` property. (The names are case-insensitive, so `handler="download"` and `handler="DOWNLOAD"` will trigger the same action.) You can also add your own handler by implementing the -`execute` event. Note that you can use *both* a built-in handler and your own `execute` -function in the same action. +`execute` event. + +Note that you can use _both_ a built-in handler and your own `execute` +function in the same action. The `execute` function is passed a `NodeMinimalEntry` as its +parameter (see the [Document Library model](document-library.model.md) page for more +information) which contains full details of the item that the action is operating on. For +example, with `handler="delete"` you could use `execute` to show a message with the name, +type and other details of the item just deleted: + +```html + + + + +``` + +```ts + myCustomActionAfterDelete(event) { + let entry = event.value.entry; + + let item = ""; + + if (entry.isFile) { + item = "file"; + } else if (entry.isFolder) { + item = "folder" + } + + this.notificationService.openSnackMessage(`Deleted ${item} "${entry.name}" `, 20000); + } +``` + +![Custom delete message screenshot](docassets/images/ContentActSnackMessage.png) ### Built-in action examples @@ -149,7 +181,7 @@ You can also implement the `permissionEvent` to handle permission errors and use the [Notification service](notification.service.md) to show a message. ```html - + + + + + ## See also -- [Document list component](document-list.component.md) -- [Document actions service](document-actions.service.md) -- [Folder actions service](folder-actions.service.md) - \ No newline at end of file +- [Document list component](document-list.component.md) +- [Document actions service](document-actions.service.md) +- [Folder actions service](folder-actions.service.md) + diff --git a/docs/content-metadata.component.md b/docs/content-metadata.component.md new file mode 100644 index 0000000000..5a679ab077 --- /dev/null +++ b/docs/content-metadata.component.md @@ -0,0 +1,113 @@ +# Content Metadata Card component + + + + + + +Allows a user to display and edit metadata related to a node. + + + +## Properties + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| node | MinimalNodeEntryEntity | - | (**required**) The node entity to fetch metadata about | +| displayEmpty | boolean | false | Display empty values in card view or not | +| preset | string | "*" | The metadata preset's name, which defines aspects and their properties | + +## Basic Usage + +The component shows metadata related to the given node. The component uses the card view component to render the properties of metadata aspects. +The different aspects and their properties to be shown can be configured as application config preset, for details about it see the related section below. By default the component only shows the basic properties of the node. Clicking on the pencil icon at the bottom, renders the underlying card view component in edit mode enabling to edit and update the metadata properties. + +```html + + +``` + +## Application config presets + +In the application config file you can define different presets for the metadata component or override the default preset. The **default** preset is "*" if not set, meaning the component will display every aspects and properties of the nodes without filtering. One can think about presets as **whitelist filters** for the content metadata component. + +Beside the default preset you can define as many presets as you want, if you'd like to use different metadata components with different presets. + +To understand presets better, you can have a look at on the following different example configurations. + +### Mimicking the default "default" preset + +If you don't have any preset configured manually in you application config, this would be equivalent as if you had the application config as defined below: + +```json +... +"content-metadata": { + "presets": { + "default": "*" + } +} +... +``` + +### Whitelisting only a few aspects in the default preset + +If you want to restrict to only a few aspects (e.g.: exif, your-custom-aspect), you have to use the name of that particular aspect to be able to whitelist it. In case of exif aspect this is "exif:exif". + +```json +... +"content-metadata": { + "presets": { + "default": { + "custom:aspect": "*", + "exif:exif": "*" + } + } +} +... +``` + +### Whitelisting only a few properties of a few aspects in the default preset + +If you want to filter more, you can do this on property level also. For this, you have to list the names of whitelisted aspect properties in an array of strings. Again, for identifying a property, you have to use its name. + +```json +... +"content-metadata": { + "presets": { + "default": { + "custom:aspect": "*", + "exif:exif": [ "exif:width", "exif:height"] + } + } +} +... +``` + +### Whitelisting only a few properties of a few aspects in a custom preset + +And finally, you can create any custom aspect following the same rules. + +```json +... +"content-metadata": { + "presets": { + "default": "*", + "kitten-images": { + "custom:aspect": "*", + "exif:exif": [ "exif:width", "exif:height"] + } + } +} +... +``` + +## What happens when there is a whitelisted aspect in the config but the given node doesn't relate to that aspect + +Nothing, this aspect (as it is not related to the node) will be simply ignored and not be displayed. The aspects to be displayed are calculated as an intersection of the preset's aspects and the aspects related to the node. + + + + diff --git a/docs/content-node-dialog.service.md b/docs/content-node-dialog.service.md new file mode 100644 index 0000000000..edb0a17451 --- /dev/null +++ b/docs/content-node-dialog.service.md @@ -0,0 +1,69 @@ +# Content Node Dialog service + +Displays and manages dialogs for selecting content to open, copy or upload. + +## Methods + +`openFileBrowseDialogByFolderId(folderNodeId: string): Observable`
+Opens a file browser at a chosen folder location. + +`openFolderBrowseDialogByFolderId(folderNodeId: string): Observable`
+Opens a folder browser at a chosen folder location. + +`openFileBrowseDialogBySite(): Observable`
+Opens a file browser at a chosen site location. + +`openFolderBrowseDialogBySite(): Observable`
+Opens a folder browser at a chosen site location. + +`openCopyMoveDialog(action: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Observable`
+Opens a dialog to copy or move an item to a new location. + +`openUploadFileDialog(action: string, contentEntry: MinimalNodeEntryEntity): Observable`
+Opens a dialog to choose a file to upload. + +`openUploadFolderDialog(action: string, contentEntry: MinimalNodeEntryEntity): Observable`
+Opens a dialog to choose a folder to upload. + +`close()`
+Closes the currently open dialog. + +## Details + +The `openXXX` methods return an +[Observable](http://reactivex.io/rxjs/manual/overview.html#observable) that you can subscribe +to in order to get the information from the result: + +```ts +import { ContentNodeDialogService } from '@adf/content-services' + + +constructor(private contentDialogService: ContentNodeDialogService){} + +yourFunctionOnCopyOrMove(){ + this.contentDialogService + .openCopyMoveDialog(actionName, targetNode, neededPermissionForAction) + .subscribe((selections: MinimalNodeEntryEntity[]) => { + // place your action here on operation success! + }); +} +``` + +The `openXXXByFolderId` methods let you set the initial folder location of the browser +using a folder ID string. This can be obtained from the `id` property of a +[MinimalNodeEntryEntity](document-library.model.md) object (returned from a previous +dialog operation, say) or be set to one of the well-known names "-my-" , "-shared-" or +"-root-". + +The `openCopyMoveDialog` and `openUploadXXX` methods require the following parameters: + +| Name | Type | Description | +| ---- | ---- | ----------- | +| action | string | The label for the confirm button of the dialog. | +| contentEntry | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/MinimalNode.md) | The node we want to be copied/moved or uploaded. | +| neededPermissionForAction | string | (`openCopyMoveDialog` only) Permission required to perform the relative action (eg: copy will need the 'update' permission ). | + +## See Also + +- [Content node selector panel component](content-node-selector-panel.component.md) +- [Content node selector component](content-node-selector.component.md) diff --git a/docs/content-node-selector-panel.component.md b/docs/content-node-selector-panel.component.md new file mode 100644 index 0000000000..afea02f65e --- /dev/null +++ b/docs/content-node-selector-panel.component.md @@ -0,0 +1,47 @@ +# Content Node Selector Panel component + +Opens a [Content Node Selector](content-node-selector.component.md) in its own dialog window. + +## Basic Usage + +```html + + +``` + +### Properties + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| currentFolderId | string | null | Node ID of the folder currently listed | +| dropdownHideMyFiles | boolean | false | Hide the "My Files" option added to the site list by default. [See More](sites-dropdown.component.md) | +| dropdownSiteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | | custom site for site dropdown same as siteList. [See More](sites-dropdown.component.md#properties) | +| rowFilter | RowFilter | null | Custom row filter function. [See More](document-list.component.md#custom-row-filter) | +| imageResolver | ImageResolver | null | Custom image resolver function. [See More](document-list.component.md#custom-image-resolver) | +| pageSize | number | 10 | Number of items shown per page in the list | + +### Events + +| Name | Description | +| ---- | ----------- | +| select | Emitted when the user has selected an item | + +## Details + +This component opens a _content node selector_ in its own dialog window. This behaves a lot like the +standard file open/save dialogs used by applications to choose files. Full details are given in the +[Content Node Selector component](content-node-selector.component.md) page (this is similar but does +not manage the dialog window for you). Also, the +[Content Node Dialog service](content-node-dialog.service.md) has several methods that give you +finer control over the behavior of the dialog. + +## See also + +- [Content Node Selector component](content-node-selector.component.md) +- [Content Node Dialog service](content-node-dialog.service.md) diff --git a/docs/content-node-selector.component.md b/docs/content-node-selector.component.md index 509d1615eb..db45add79b 100644 --- a/docs/content-node-selector.component.md +++ b/docs/content-node-selector.component.md @@ -6,50 +6,22 @@ Allows a user to select items from a Content Services repository. ## Basic Usage -```ts -import { MatDialog } from '@angular/material'; - ... - -constructor(dialog: MatDialog ... ) {} - -openSelectorDialog() { - data: ContentNodeSelectorComponentData = { - title: "Choose an item", - currentFolderId: someFolderId, - select: new EventEmitter() - }; - - this.dialog.open( - ContentNodeSelectorComponent, - { - data, panelClass: 'adf-content-node-selector-dialog', - width: '630px' - } - ); - - data.select.subscribe((selections: MinimalNodeEntryEntity[]) => { - // Use or store selection... - - this.dialog.closeAll(); - }); -} - -``` - ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | title | string | "" | Text shown at the top of the selector | | currentFolderId | string | null | Node ID of the folder currently listed | -| rowFilter | RowFilter | null | Custom row filter function | -| imageResolver | ImageResolver | null | Custom image resolver function | +| dropdownHideMyFiles | boolean | false | Hide the "My Files" option added to the site list by default. [See More](sites-dropdown.component.md) | +| dropdownSiteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | | custom site for site dropdown same as siteList. [See More](sites-dropdown.component.md#properties) | +| rowFilter | RowFilter | null | Custom row filter function. [See More](document-list.component.md#custom-row-filter) | +| imageResolver | ImageResolver | null | Custom image resolver function. [See More](document-list.component.md#custom-image-resolver) | | pageSize | number | 10 | Number of items shown per page in the list | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | select | Emitted when the user has selected an item | ## Details @@ -62,7 +34,7 @@ of Copy/Move actions (see the [Content Action component](content-action.componen more information). Unlike most components, the Content Node Selector is typically shown in a dialog box -rather than the main page. You can use the +rather than the main page and you are responsible for opening the dialog yourself. You can use the [Angular Material Dialog](https://material.angular.io/components/dialog/overview) for this, as shown in the usage example. ADF provides the `ContentNodeSelectorComponentData` interface to work with the Dialog's @@ -78,6 +50,52 @@ interface ContentNodeSelectorComponentData { } ``` +If you don't want to manage the dialog yourself then it is easier to use the +[Content Node Selector Panel component](content-node-selector-panel.component.md), or the +methods of the [Content Node Dialog service](content-node-dialog.service.md), which create +the dialog for you. + +### Usage example + +```ts +import { MatDialog } from '@angular/material'; +import { ContentNodeSelectorComponentData, ContentNodeSelectorComponent} from '@adf/content-services' +import { Subject } from 'rxjs/Subject'; + ... + +constructor(dialog: MatDialog ... ) {} + +openSelectorDialog() { + data: ContentNodeSelectorComponentData = { + title: "Choose an item", + currentFolderId: someFolderId, + select: new Subject() + }; + + this.dialog.open( + ContentNodeSelectorComponent, + { + data, panelClass: 'adf-content-node-selector-dialog', + width: '630px' + } + ); + + data.select.subscribe((selections: MinimalNodeEntryEntity[]) => { + // Use or store selection... + }, + (error)=>{ + //your error handling + }, + ()=>{ + //action called when an action or cancel is clicked on the dialog + this.dialog.closeAll(); + }); +} +``` + +All the results will be streamed to the select [subject](http://reactivex.io/rxjs/manual/overview.html#subject) present in the `ContentNodeSelectorComponentData` object passed to the dialog. +When the dialog action is selected by clicking, the `data.select` stream will be completed. + ### RowFilter and ImageResolver The Content Node Selector uses a [Document List](document-list.component.md) to display the @@ -88,9 +106,8 @@ a row filter to hide document nodes in a folder selector. See the [Advanced Usage and Customization](document-list.component.md#advanced-usage-and-customization) section of the Document List page to learn how these functions are implemented. - - ## See also -- [Document list component](document-list.component.md) - \ No newline at end of file +- [Document list component](document-list.component.md) +- [Content Node Selector Panel component](content-node-selector-panel.component.md) +- [Content Node Dialog service](content-node-dialog.service.md) diff --git a/docs/content.service.md b/docs/content.service.md index ad9a55a5c3..fbe9eb5928 100644 --- a/docs/content.service.md +++ b/docs/content.service.md @@ -28,7 +28,7 @@ some cases by viewing it directly in the browser via a special URL that referenc the Blob. For example, you could use the Blob interface to construct an image in the [PNG format](https://en.wikipedia.org/wiki/Portable_Network_Graphics). Since PNG is a format the browser can display, you could use the Blob's URL in an -<img> element to view the image within the page. Alternatively, you could let +<img> element to view the image within the page. Alternatively, you could let the user download it as a PNG file. The `downloadBlob` method starts a download of the Blob data to the `filename` @@ -40,20 +40,14 @@ for details of how a Blob's contents are assembled from the constructor argument Use `createdTrustedUrl` to generate a URL string for a Blob. The URL refers to the Blob as though it were a file but it is actually an object stored in memory, so it does not persist across browser sessions. This URL can be used much like any -other, so you could use it for the `src` attribute of an <img> element or the +other, so you could use it for the `src` attribute of an <img> element or the `href` of a download link. Note that while the URL is 'trusted', the data it contains is not necessarily trustworthy unless you can vouch for it yourself; be careful that the data doesn't expose your app to [Cross Site Scripting](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. - - ## See also -- [Cookie service](cookie.service.md) -- [Storage service](storage.service.md) - - - - +- [Cookie service](cookie.service.md) +- [Storage service](storage.service.md) diff --git a/docs/content.widget.md b/docs/content.widget.md index f1fa2b000f..903a1489e7 100644 --- a/docs/content.widget.md +++ b/docs/content.widget.md @@ -1,18 +1,6 @@ # Activiti Content Component -The component shows the content preview. - - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) - - - - +Shows the content preview. ## Basic Usage @@ -27,11 +15,11 @@ The component shows the content preview. The recommended set of properties can be found in the following table: | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | contentId | string | | The content id to show. | ### Events | Name | Description | -| --- | --- | -| contentClick | Invoked when the content is clicked. | \ No newline at end of file +| ---- | ----------- | +| contentClick | Invoked when the content is clicked. | diff --git a/docs/context-menu.directive.md b/docs/context-menu.directive.md index 3f0dd41fe0..c4067fb0f0 100644 --- a/docs/context-menu.directive.md +++ b/docs/context-menu.directive.md @@ -1,15 +1,6 @@ # Context Menu directive - - - - -- [Basic Usage](#basic-usage) -- [Details](#details) - - - - +Adds a context menu to a component. ## Basic Usage @@ -48,8 +39,8 @@ export class MyComponent implements OnInit { ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| showIcons | boolean | false | Render defined icons | +| ---- | ---- | ------- | ----------- | +| showIcons | boolean | false | Render defined icons | ## Details diff --git a/docs/create-process-attachment.component.md b/docs/create-process-attachment.component.md index a4173999e2..d499d9e23c 100644 --- a/docs/create-process-attachment.component.md +++ b/docs/create-process-attachment.component.md @@ -17,12 +17,12 @@ Displays Upload Component (Drag and Click) to upload the attachment to a specifi ### Properties | Name | Type | Description | -| --- | --- | --- | -| processInstanceId | string | (**required**): The ID of the process instance to display | +| ---- | ---- | ----------- | +| processInstanceId | `string` | (required) The ID of the process instance to display. | ### Events -| Name | Description | -| --- | --- | -| error | Raised when the error occurred while creating/uploading the attachment by the user from within the component | -| success | Raised when the attachment created/uploaded successfully from within the component | +| Name | Type | Description | +| ---- | ---- | ----------- | +| error | `EventEmitter` | Emitted when an error occurs while creating or uploading an attachment from the user within the component. | +| success | `EventEmitter` | Emitted when an attachment is successfully created or uploaded from within the component. | diff --git a/docs/create-task-attachment.component.md b/docs/create-task-attachment.component.md index ed32812b0d..3528fd635e 100644 --- a/docs/create-task-attachment.component.md +++ b/docs/create-task-attachment.component.md @@ -17,12 +17,12 @@ Displays Upload Component (Drag and Click) to upload the attachment to a specifi ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | taskId | string | (**required**): The numeric ID of the task to display | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | error | Raised when the error occurred while creating/uploading the attachment by the user from within the component | | success | Raised when the attachment created/uploaded successfully from within the component | diff --git a/docs/data-column.component.md b/docs/data-column.component.md index 1e11cf73eb..f8d5d31aaf 100644 --- a/docs/data-column.component.md +++ b/docs/data-column.component.md @@ -2,24 +2,20 @@ Defines column properties for DataTable, Tasklist, Document List and other components. - +## Contents - +- [Basic Usage](#basic-usage) -- [Basic Usage](#basic-usage) - * [Properties](#properties) -- [Details](#details) - * [Automatic column header translation](#automatic-column-header-translation) - * [Custom tooltips](#custom-tooltips) - * [Column Template](#column-template) - * [Styling Techniques](#styling-techniques) - + [Custom icons for selected rows](#custom-icons-for-selected-rows) - + [Hiding columns on small screens](#hiding-columns-on-small-screens) -- [See also](#see-also) + - [Properties](#properties) - +- [Details](#details) - + - [Automatic column header translation](#automatic-column-header-translation) + - [Custom tooltips](#custom-tooltips) + - [Column Template](#column-template) + - [Styling Techniques](#styling-techniques) + +- [See also](#see-also) ## Basic Usage @@ -37,17 +33,16 @@ Defines column properties for DataTable, Tasklist, Document List and other compo ### Properties -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| key | string | | Data source key, can be either column/property key like `title` or property path like `createdBy.name` | -| type | string | text | Value type for the column. Possible settings are 'text', 'image', 'date', 'fileSize' and 'location'. | -| format | string | | Value format (if supported by components), for example format of the date | -| sortable | boolean | true | Toggles ability to sort by this column, for example by clicking the column header | -| title | string | | Display title of the column, typically used for column headers. You can use the i18n resouce key to get it translated automatically. | -| template | `TemplateRef` | | Custom column template | -| sr-title | string | | Screen reader title, used for accessibility purposes | -| class | string | | Additional CSS class to be applied to column (header and cells) | -| formatTooltip | Function | | Custom tooltip formatter function. | +| Name | Type | Description | +| ---- | ---- | ----------- | +| key | `string` | Data source key. Can be either a column/property key like `title` or a property path like `createdBy.name`. | +| type | `string` | Value type for the column. Possible settings are 'text', 'image', 'date', 'fileSize' and 'location'.
Default value: `'text'` | +| format | `string` | Value format (if supported by the parent component), for example format of the date. | +| sortable | `boolean` | Toggles ability to sort by this column, for example by clicking the column header.
Default value: `true` | +| title | `string` | Display title of the column, typically used for column headers. You can use the i18n resource key to get it translated automatically.
Default value: `''` | +| formatTooltip | `Function` | Custom tooltip formatter function. | +| srTitle | `string` | Title to be used for screen readers. | +| cssClass | `string` | Additional CSS class to be applied to column (header and cells). | ## Details @@ -105,9 +100,9 @@ You can provide custom column/cell templates that may contain other Angular comp Every cell in the DataTable component is bound to the dynamic data context containing the following properties: | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | data | [DataTableAdapter](DataTableAdapter.md) | Data adapter instance. | -| row | [DataRow](DataTableAdapter.md) | Current data row instance. | +| row | [DataRow](DataTableAdapter.md) | Current data row instance. | | col | [DataColumn](DataTableAdapter.md) | Current data column instance. | You can use all three properties to gain full access to underlying data from within your custom templates. @@ -145,7 +140,7 @@ You may want using **row** api to get raw value access. ``` -Use **data** api to get values with post-processing, like datetime/icon conversion._ +Use **data** api to get values with post-processing, like datetime/icon conversion.\_ In the Example below we will prepend `Hi!` to each file and folder name in the list: @@ -180,7 +175,7 @@ You can add a custom CSS class to a column using its `class` property. This is u many purposes - some examples are given below. #### Custom icons for selected rows - + Custom styling can be used to change the look and feel of the icon for the selected rows. Let's start by assigning an "image-table-cell" class to the thumbnail column: @@ -272,10 +267,12 @@ Now you can declare columns and assign `desktop-only` class where needed: ![Responsive Mobile](docassets/images/responsive-mobile.png) + + ## See also -- [Document list component](document-list.component.md) -- [Datatable component](datatable.component.md) -- [Task list component](task-list.component.md) - \ No newline at end of file +- [Document list component](document-list.component.md) +- [Datatable component](datatable.component.md) +- [Task list component](task-list.component.md) + diff --git a/docs/datatable.component.md b/docs/datatable.component.md index 292893a4d9..f86ea0fcde 100644 --- a/docs/datatable.component.md +++ b/docs/datatable.component.md @@ -6,31 +6,23 @@ Displays data as a table with customizable columns and presentation. See it live: [DataTable Quickstart](https://embed.plnkr.co/80qr4YFBeHjLMdAV0F6l/) - +## Contents - +- [Basic usage](#basic-usage) -- [Basic usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - * [Supplying data for the table](#supplying-data-for-the-table) - * [Customizing columns](#customizing-columns) - * [DataTable DOM Events](#datatable-dom-events) - * [Custom Empty content template](#custom-empty-content-template) - * [Loading content template](#loading-content-template) - * [Events](#events-1) - + [row-keyup DOM event](#row-keyup-dom-event) - + [rowClick event](#rowclick-event) - + [rowDblClick event](#rowdblclick-event) - + [showRowContextMenu event](#showrowcontextmenu-event) - + [showRowActionsMenu event](#showrowactionsmenu-event) - + [executeRowAction event](#executerowaction-event) -- [See also](#see-also) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [Supplying data for the table](#supplying-data-for-the-table) + - [Customizing columns](#customizing-columns) + - [DataTable DOM Events](#datatable-dom-events) + - [Custom Empty content template](#custom-empty-content-template) + - [Loading content template](#loading-content-template) + - [Events](#events-1) + +- [See also](#see-also) ## Basic usage @@ -127,26 +119,26 @@ export class DataTableDemo { ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | -| rowStyle | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples | -| rowStyleClass | string | | The CSS class to apply to every row | +| ---- | ---- | ------- | ----------- | +| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | +| rowStyle | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples | +| rowStyleClass | string | | The CSS class to apply to every row | | data | DataTableAdapter | instance of **ObjectDataTableAdapter** | data source | -| rows | Object[] | [] | The rows that the datatable should show | +| rows | Object\[] | \[] | The rows that the datatable should show | | multiselect | boolean | false | Toggles multiple row selection, renders checkboxes at the beginning of each row | | actions | boolean | false | Toggles data actions column | -| actionsPosition | string (left\|right) | right | Position of the actions dropdown menu. | -| fallbackThumbnail | string | | Fallback image for row where thumbnail is missing| +| actionsPosition | string (left\|right) | right | Position of the actions dropdown menu. | +| fallbackThumbnail | string | | Fallback image for row where thumbnail is missing | | contextMenu | boolean | false | Toggles custom context menu for the component | | allowDropFiles | boolean | false | Toggle file drop support for rows (see **ng2-alfresco-core/UploadDirective** for more details) | -| loading | boolean | false | Flag that indicates if the datatable is in loading state and needs to show the loading template. Read the documentation above to see how to configure a loading template | +| loading | boolean | false | Flag that indicates if the datatable is in loading state and needs to show the loading template. Read the documentation above to see how to configure a loading template | | showHeader | boolean | true | Toggles header visibility | -| selection | DataRow[] | [] | Contains selected rows | +| selection | DataRow\[] | \[] | Contains selected rows | ### Events -| Name | Description -| --- | --- | +| Name | Description | +| ---- | ----------- | | [rowClick](#rowclick-event) | Emitted when user clicks the row | | [rowDblClick](#rowdblclick-event) | Emitted when user double-clicks the row | | [showRowContextMenu](#showrowcontextmenu-event) | Emitted before context menu is displayed for a row | @@ -174,12 +166,12 @@ Below are the DOM events raised by DataTable component. These events bubble up the component tree and can be handled by any parent component. | Name | Description | -| --- | --- | +| ---- | ----------- | | row-click | Raised when user clicks a row | | row-dblclick | Raised when user double-clicks a row | | row-select | Raised after user selects a row | | row-unselect | Raised after user unselects a row | -| row-keyup | Raised on the 'keyup' event for the focused row. | +| row-keyup | Raised on the 'keyup' event for the focused row. | For example: @@ -250,8 +242,8 @@ You can use any HTML layout or Angular component as a content of the empty templ ``` -| Name | Type | Default | Description -| --- | --- | --- | --- | +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | | emptyListImageUrl | String | empty_doc_lib.svg | The default image used as background | | emptyMsg | String | This list is empty | The default title message | | dragDropMsg | String | Drag and drop | The default drag and drop message | @@ -462,11 +454,13 @@ where you can handle the process, inspect the action payload and all custom prop and do corresponding actions. + + ## See also -- [Data column component](data-column.component.md) -- [Pagination component](pagination.component.md) -- [DataTableAdapter](DataTableAdapter.md) -- [Document list component](document-list.component.md) - \ No newline at end of file +- [Data column component](data-column.component.md) +- [Pagination component](pagination.component.md) +- [DataTableAdapter](DataTableAdapter.md) +- [Document list component](document-list.component.md) + diff --git a/docs/docassets/images/ContentActSnackMessage.png b/docs/docassets/images/ContentActSnackMessage.png new file mode 100644 index 0000000000000000000000000000000000000000..ef100c31f5c5e6bdc27d35519873ffb6e12a4792 GIT binary patch literal 3982 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 literal 0 HcmV?d00001 diff --git a/docs/docassets/images/ContentMetadata.png b/docs/docassets/images/ContentMetadata.png new file mode 100644 index 0000000000000000000000000000000000000000..f643f199e43df327d6d77e69ca1a9d512e1d0d4c GIT binary patch literal 42884 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

*BKBtU^GIznl#T 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 literal 0 HcmV?d00001 diff --git a/docs/docassets/images/process-attachment-list.png b/docs/docassets/images/process-attachment-list.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c6c704219dde0eac1bae8dd8db805b0ab99608 GIT binary patch literal 13630 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 literal 0 HcmV?d00001 diff --git a/docs/docassets/images/select-apps-dialog.png b/docs/docassets/images/select-apps-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f9501a924d4abb1a864adbb6f232166e22bee4 GIT binary patch literal 48484 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 z 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 W-S#LRUP%A9ZJT#}$&cCl{eJ-odkRtj literal 0 HcmV?d00001 diff --git a/docs/docassets/images/sidebar-action-menu-button.png b/docs/docassets/images/sidebar-action-menu-button.png new file mode 100644 index 0000000000000000000000000000000000000000..44523e8660d523f42d450fe0afe81e8ef4a4130c GIT binary patch literal 7461 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 Z$u!R6OP9JklOPpGDBM?(DM1-M`!AfVs~!LV literal 0 HcmV?d00001 diff --git a/docs/docassets/images/start_process.png b/docs/docassets/images/start_process.png new file mode 100644 index 0000000000000000000000000000000000000000..ac808e5c63cf6789496676d88fb5c252cedb5f5a GIT binary patch literal 86013 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 literal 0 HcmV?d00001 diff --git a/docs/document-list.component.md b/docs/document-list.component.md index e5514502d4..f23ec46e80 100644 --- a/docs/document-list.component.md +++ b/docs/document-list.component.md @@ -4,37 +4,35 @@ Displays the documents from a repository. ![Custom columns](docassets/images/custom-columns.png) - +## Contents - +- [Basic Usage](#basic-usage) -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - * [DOM Events](#dom-events) - * [Pagination strategy](#pagination-strategy) - * [Data Sources](#data-sources) - + [Node ID](#node-id) - + [Repository aliases](#repository-aliases) - + [DocumentList aliases](#documentlist-aliases) - * [Setting default folder](#setting-default-folder) - * [Calling DocumentList api directly](#calling-documentlist-api-directly) - * [Underlying node object](#underlying-node-object) - * [Custom columns](#custom-columns) - * [Date Column](#date-column) - * [Location Column](#location-column) - * [Actions](#actions) - * [Navigation mode](#navigation-mode) -- [Advanced usage and customization](#advanced-usage-and-customization) - * [Custom row filter](#custom-row-filter) - * [Custom image resolver](#custom-image-resolver) - * [Custom 'empty folder' template](#custom-empty-folder-template) -- [See also](#see-also) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [DOM Events](#dom-events) + - [Pagination strategy](#pagination-strategy) + - [Data Sources](#data-sources) + - [Setting default folder](#setting-default-folder) + - [Calling DocumentList api directly](#calling-documentlist-api-directly) + - [Underlying node object](#underlying-node-object) + - [Custom columns](#custom-columns) + - [Date Column](#date-column) + - [Location Column](#location-column) + - [Actions](#actions) + - [Navigation mode](#navigation-mode) + +- [Advanced usage and customization](#advanced-usage-and-customization) + + - [Custom row filter](#custom-row-filter) + - [Custom image resolver](#custom-image-resolver) + - [Custom 'empty folder' template](#custom-empty-folder-template) + - [Custom 'permission denied' template](#custom-permission-denied-template) + +- [See also](#see-also) ## Basic Usage @@ -49,56 +47,55 @@ Displays the documents from a repository. ### Properties -The properties currentFolderId, folderNode and node are the entry initialization properties of the document list. They cannot be used together, choose the one that suites more your use case. - -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| permissionsStyle | [PermissionStyleModel[]](permissions-style.model.md) | null | Define a set of CSS styles styles to apply depending on the permission of the user on that node. See the [Permission Style model](permissions-style.model.md) page for further details and examples. | -| locationFormat | string | '/' | The default route for all the location-based columns (if declared). | -| navigate | boolean | true | Toggles navigation to folder content or file preview | -| navigationMode | string (click,dblclick) | dblclick | User interaction for folder navigation or file preview | -| thumbnails | boolean | false | Show document thumbnails rather than icons | -| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | -| multiselect | boolean | false | Toggles multiselect mode | -| contentActions | boolean | false | Toggles content actions for each row | -| contentActionsPosition | string (left\|right) | right | Position of the content actions dropdown menu. | -| contextMenuActions | boolean | false | Toggles context menus for each row | -| emptyFolderImageUrl | string | assets/images/empty_doc_lib.svg | Custom image for empty folder | -| allowDropFiles | boolean | false | Toggle file drop support for rows (see **ng2-alfresco-core/UploadDirective** for more details) | -| sorting | string[] | | Defines default sorting. The format is an array of 2 strings `[key, direction]` i.e. `['name', 'desc']` or `['name', 'asc']`. Set this value only if you want to override default sorting detected by the component based on columns. | -| rowStyle | string | | The inline style to apply to every row, see [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) docs for more details and usage examples | -| rowStyleClass | string | | The CSS class to apply to every row | -| loading | boolean | false | Toggles the loading state and animated spinners for the component. Used in combination with `navigate=false` to perform custom navigation and loading state indication. | -| rowFilter | `RowFilter` | | Custom row filter, [see more](#custom-row-filter). | -| imageResolver | `ImageResolver` | | Custom image resolver, [see more](#custom-image-resolver). | -| currentFolderId | string | null | The ID of the folder node to display or a reserved string alias for special sources (see **Data Sources**) | -| folderNode | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md) | null | Currently displayed folder node | -| node | [NodePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodePaging.md) | null | Document list will show all the nodes contained in the NodePaging entity | -| maxItems | number | Default value is stored into user preference settings | determine the size of the page for the element into document list | -| skipCount | number | 0 | element to skip over for pagination purpose | -| enableInfiniteScrolling | boolean | true | Enable documentlist to work into infinite scrolling mode | +| Name | Type | Description | +| ---- | ---- | ----------- | +| permissionsStyle | `PermissionStyleModel[]` | Define a set of CSS styles styles to apply depending on the permission of the user on that node. See the Permission Style model page for further details and examples.
Default value: `[]` | +| locationFormat | `string` | The default route for all the location-based columns (if declared).
Default value: `'/'` | +| navigate | `boolean` | Toggles navigation to folder content or file preview
Default value: `true` | +| navigationMode | `string` | User interaction for folder navigation or file preview. Valid values are "click" and "dblclick".
Default value: `DocumentListComponent.DOUBLE_CLICK_NAVIGATION` | +| thumbnails | `boolean` | Show document thumbnails rather than icons
Default value: `false` | +| selectionMode | `string` | Row selection mode. Can be null, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows.
Default value: `'single'` | +| multiselect | `boolean` | Toggles multiselect mode
Default value: `false` | +| contentActions | `boolean` | Toggles content actions for each row
Default value: `false` | +| contentActionsPosition | `string` | Position of the content actions dropdown menu. Can be set to "left" or "right".
Default value: `'right'` | +| contextMenuActions | `boolean` | Toggles context menus for each row
Default value: `false` | +| emptyFolderImageUrl | `string` | Custom image for empty folder
Default value: `'./assets/images/empty_doc_lib.svg'` | +| allowDropFiles | `boolean` | Toggle file drop support for rows (see Upload Directive for further details
Default value: `false` | +| sorting | `string[]` | Defines default sorting. The format is an array of 2 strings `[key, direction]` i.e. `['name', 'desc']` or `['name', 'asc']`. Set this value only if you want to override the default sorting detected by the component based on columns. | +| rowStyle | `string` | The inline style to apply to every row. See the Angular NgStyle docs for more details and usage examples. | +| rowStyleClass | `string` | The CSS class to apply to every row | +| loading | `boolean` | Toggles the loading state and animated spinners for the component. Used in combination with `navigate=false` to perform custom navigation and loading state indication.
Default value: `false` | +| rowFilter | `any` | Custom row filter
Default value: `null` | +| imageResolver | `any` | Custom image resolver
Default value: `null` | +| currentFolderId | `string` | The ID of the folder node to display or a reserved string alias for special sources
Default value: `null` | +| folderNode | `MinimalNodeEntryEntity` | Currently displayed folder node
Default value: `null` | +| node | `NodePaging` | The Document list will show all the nodes contained in the NodePaging entity
Default value: `null` | +| maxItems | `number` | Default value is stored into user preference settings | +| skipCount | `number` | Number of elements to skip over for pagination purposes
Default value: `0` | +| enableInfiniteScrolling | `boolean` | Set document list to work in infinite scrolling mode
Default value: `false` | ### Events -| Name | Description | -| --- | --- | -| nodeClick | emitted when user clicks a list node | -| nodeDblClick | emitted when user double-clicks list node | -| folderChange | emitted once current display folder has changed | -| preview | emitted when user acts upon files with either single or double click (depends on `navigation-mode`), recommended for Viewer components integration | -| permissionError | emitted when user is attempting to create a folder via action menu without having the permission to do it | -| ready | emitted when the documentList is ready and loads all the elements| -| error | emitted when API fails to get documentList data| +| Name | Type | Description | +| ---- | ---- | ----------- | +| nodeClick | `EventEmitter` | Emitted when the user clicks a list node | +| nodeDblClick | `EventEmitter` | Emitted when the user double-clicks a list node | +| folderChange | `EventEmitter` | Emitted when the current display folder changes | +| preview | `EventEmitter` | Emitted when the user acts upon files with either single or double click (depends on `navigation-mode`). Useful for integration with the Viewer component. | +| ready | `EventEmitter` | Emitted when the Document List has loaded all items and is ready for use | +| error | `EventEmitter` | Emitted when the API fails to get the Document List data | ## Details +The properties currentFolderId, folderNode and node are the entry initialization properties of the document list. They cannot be used together, so choose the one that suits your use case best. + ### DOM Events Below are the DOM events the DocumentList component emits. All of them are `bubbling`, meaning you can handle them in any component up the parent hierarchy, even if DocumentList is wrapped by another component(s). | Name | Description | -| --- | --- | +| ---- | ----------- | | node-click | Raised when user clicks the node | | node-dblclick | Raised when user double-clicks the node | | node-select | Raised when user selects a node | @@ -134,8 +131,8 @@ Below is a basic example of handling DOM events in the parent elements. The Document List by default supports 2 type of pagination, the **finite** and the **infinite** pagination. -- With the **finite** pagination you must provide document list with 2 parameters : maxItems and skipCount. -- With the **infinite** pagination you must provide documentlist with 3 parameters : enableInfiniteScrolling, maxItems and skipCount. +- With the **finite** pagination you must provide document list with 2 parameters : maxItems and skipCount. +- With the **infinite** pagination you must provide documentlist with 3 parameters : enableInfiniteScrolling, maxItems and skipCount. ### Data Sources @@ -149,20 +146,20 @@ The unique identifier of the Node. Gets automatically updated when you perform n You can use one of the well-known reserved aliases: -- `-root-` -- `-shared-` -- `-my-` +- `-root-` +- `-shared-` +- `-my-` #### DocumentList aliases The DocumentList component also provides support for the following reserved aliases you can use: -- `-trashcan-`, -- `-sharedlinks-` -- `-sites-` -- `-mysites-` -- `-favorites-` -- `-recent-` +- `-trashcan-`, +- `-sharedlinks-` +- `-sites-` +- `-mysites-` +- `-favorites-` +- `-recent-` Note that due to specific origin of the data the sources above do not support navigation. You may want handling single and double clicks yourself to perform navigation to other sources. @@ -177,13 +174,13 @@ There's a possibility to set the default location format using "locationFormat" The default column layout for non-reserved views is: -- Icon -- Name -- Size -- Modified (date) -- Modified by +- Icon +- Name +- Size +- Modified (date) +- Modified by -__Trashcan__ +**Trashcan** ```html ... + ... ` }) @@ -584,11 +581,11 @@ By default DocumentList component uses 'double-click' mode for navigation. That means user will see the contents of the folder by double-clicking its name or icon (similar to Google Drive behaviour). However it is possible switching to other modes, like single-click navigation for example. - + The following navigation modes are supported: -- **click** -- **dblclick** +- **click** +- **dblclick** The following example switches navigation to single clicks: @@ -756,23 +753,26 @@ This can be changed by means of the custom html template: That will give the following output: ![Custom no permission](docassets/images/no-permission-custom.png) + + + ## See also -- [Datatable component](datatable.component.md) -- [Data column component](data-column.component.md) -- [Pagination component](pagination.component.md) -- [Infinite pagination component](infinite-pagination.component.md) -- [Sites dropdown component](sites-dropdown.component.md) -- [Metadata indicators](metadata-indicators.md) -- [Document library model](document-library.model.md) -- [Nodes api service](nodes-api.service.md) -- [Breadcrumb component](breadcrumb.component.md) -- [Content action component](content-action.component.md) -- [Content node selector component](content-node-selector.component.md) -- [Document list service](document-list.service.md) -- [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) -- [Permissions style model](permissions-style.model.md) -- [Version manager component](version-manager.component.md) - \ No newline at end of file +- [Datatable component](datatable.component.md) +- [Data column component](data-column.component.md) +- [Pagination component](pagination.component.md) +- [Infinite pagination component](infinite-pagination.component.md) +- [Sites dropdown component](sites-dropdown.component.md) +- [Metadata indicators](metadata-indicators.md) +- [Document library model](document-library.model.md) +- [Nodes api service](nodes-api.service.md) +- [Breadcrumb component](breadcrumb.component.md) +- [Content action component](content-action.component.md) +- [Content node selector component](content-node-selector.component.md) +- [Document list service](document-list.service.md) +- [Dropdown breadcrumb component](dropdown-breadcrumb.component.md) +- [Permissions style model](permissions-style.model.md) +- [Version manager component](version-manager.component.md) + diff --git a/docs/dropdown-breadcrumb.component.md b/docs/dropdown-breadcrumb.component.md index 0f0aba167b..81c28f5a18 100644 --- a/docs/dropdown-breadcrumb.component.md +++ b/docs/dropdown-breadcrumb.component.md @@ -16,22 +16,24 @@ Indicates the current position within a navigation hierarchy using a dropdown me ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | target | DocumentListComponent | (optional) DocumentList component to operate with. Upon clicks will instruct the given component to update. | | folderNode | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md) | Active node, builds UI based on `folderNode.path.elements` collection. | -| root | string | (optional) Name of the root element of the breadcrumb. You can use this property to rename "Company Home" to "Personal Files" for example. You can use i18n resource key for the property value. | +| root | string | (optional) Name of the root element of the breadcrumb. You can use this property to rename "Company Home" to "Personal Files" for example. You can use i18n resource key for the property value. | | rootId | string | (optional) The id of the root element. You can use this property to set a custom element the breadcrumb should start with. | ### Events | Name | Returned Type | Description | -| --- | --- | --- | -| navigate | [PathElementEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/PathElementEntity.md) | emitted when user clicks on a breadcrumb | +| ---- | ------------- | ----------- | +| navigate | [PathElementEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/PathElementEntity.md) | emitted when user clicks on a breadcrumb | + + ## See also -- [Document list component](document-list.component.md) -- [Breadcrumb component](breadcrumb.component.md) - \ No newline at end of file +- [Document list component](document-list.component.md) +- [Breadcrumb component](breadcrumb.component.md) + diff --git a/docs/ecm-user.model.md b/docs/ecm-user.model.md index 8cd598cc8e..ae1b75083a 100644 --- a/docs/ecm-user.model.md +++ b/docs/ecm-user.model.md @@ -14,11 +14,8 @@ it is likely to change and so its properties are not listed here. For the latest version of the class, see the [source file](https://github.com/Alfresco/alfresco-ng2-components/blob/development/lib/core/userinfo/models/ecm-user.model.ts). - - ## See also -- [Ecm user service](ecm-user.service.md) -- [People content service](people-content.service.md) -- [Bpm user model](bpm-user.model.md) - \ No newline at end of file +- [Ecm user service](ecm-user.service.md) +- [People content service](people-content.service.md) +- [Bpm user model](bpm-user.model.md) diff --git a/docs/ecm-user.service.md b/docs/ecm-user.service.md index 9d12922459..93881e2f62 100644 --- a/docs/ecm-user.service.md +++ b/docs/ecm-user.service.md @@ -24,10 +24,7 @@ See the [getPerson](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/PeopleApi.md#getPerson) method in the Alfresco JS API for more information about the REST calls used by this service. - - ## See also -- [Bpm user service](bpm-user.service.md) -- [Ecm user model](ecm-user.model.md) - \ No newline at end of file +- [Bpm user service](bpm-user.service.md) +- [Ecm user model](ecm-user.model.md) diff --git a/docs/extensibility.md b/docs/extensibility.md index 30b2f83627..7c1850baa2 100644 --- a/docs/extensibility.md +++ b/docs/extensibility.md @@ -1,10 +1,10 @@ # Form Extensibility and Customisation -_Note: it is assumed you are familiar with Alfresco Activiti form definition structure._ +_Note: it is assumed you are familiar with Alfresco Process Services (powered by Activiti) form definition structure._ -- How components and widgets are rendered on a Form -- Replacing default form widgets with custom components -- Replacing custom stencils with custom components +- How components and widgets are rendered on a Form +- Replacing default form widgets with custom components +- Replacing custom stencils with custom components ## How components and widgets are rendered on a Form @@ -63,7 +63,7 @@ formRenderingService.setComponentTypeResolver('text', customResolver, true); ### Default component mappings | Stencil Name | Field Type | Component Type | -| --- | --- | --- | +| ------------ | ---------- | -------------- | | Text | text | TextWidgetComponent | | Number | integer | NumberWidgetComponent | | Multi-line text | multi-line-text | MultilineTextWidgetComponentComponent | @@ -86,10 +86,10 @@ formRenderingService.setComponentTypeResolver('text', customResolver, true); ## Replacing default form widgets with custom components -This is a short walkthrough on replacing a standard `Text` widget with a custom component for all Alfresco Activiti forms -rendered within `` component. +This is a short walkthrough on replacing a standard `Text` widget with a custom component for all APS forms +rendered within `` component. -First let's create a simple Alfresco Activiti form with `Text` widgets: +First let's create a simple APS form with `Text` widgets: ![default text widget](docassets/images/text-default-widget.png) @@ -162,7 +162,7 @@ At runtime it should look similar to the following: ## Replacing custom stencils with custom components -This is a short walkthrough on rendering custom Alfresco Activiti stencils by means of custom Angular components. +This is a short walkthrough on rendering custom APS stencils by means of custom Angular components. ### Creating custom stencil @@ -182,13 +182,13 @@ Now you are ready to design a test form based on your custom stencil: ![custom stencil form](docassets/images/activiti-stencil-02.png) -Once wired with a new task it should look like the following within Alfresco Activiti web application: +Once wired with a new task it should look like the following within APS web application: ![custom stencil task](docassets/images/activiti-stencil-03.png) ### Creating custom widget -If you load previously created task into ADF `` component you will see something like the following: +If you load previously created task into ADF `` component you will see something like the following: ![adf stencil](docassets/images/adf-stencil-01.png) @@ -252,4 +252,11 @@ export class MyView { At runtime you should now see your custom Angular component rendered in place of the stencils: -![adf stencil runtime](docassets/images/adf-stencil-02.png) \ No newline at end of file +![adf stencil runtime](docassets/images/adf-stencil-02.png) + +## See Also + +- [Form field model](form-field.model.md) +- [Form rendering service](form-rendering.service.md) +- [Form component](form.component.md) +- [Widget component](widget.component.md) diff --git a/docs/favorites-api.service.md b/docs/favorites-api.service.md index 3660e8c227..43e47fcee0 100644 --- a/docs/favorites-api.service.md +++ b/docs/favorites-api.service.md @@ -20,8 +20,3 @@ person is the currently logged-in user. You can specify a number of `options` to modify the search further. See the [Alfresco JS API page](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/FavoritesApi.md#getfavorites) for `getFavorites` for more information. - - - - - \ No newline at end of file diff --git a/docs/file-draggable.directive.md b/docs/file-draggable.directive.md index bca9999f11..6242293718 100644 --- a/docs/file-draggable.directive.md +++ b/docs/file-draggable.directive.md @@ -1,4 +1,5 @@ # File Draggable directive + Provide drag-and-drop features for an element such as a `div`. ## Basic Usage @@ -27,7 +28,25 @@ Some sample CSS to show the drag and drop area: } ``` -Event handler implementations: +### Properties + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| file-draggable | boolean | true | Toggles between enable/disable of the drag-and-drop functionality. | + +### Events + +| Name | Description | +| ---- | ----------- | +| filesEntityDropped | Emitted when one or more files are dragged and dropped onto the draggable element, such as a `div` | +| folderEntityDropped | Emitted when a directory is dragged and dropped onto the draggable element, such as a `div` | +| filesDropped | Emitted when one or more files are dragged and dropped onto the draggable element, such as a `div` | + +## Details + +Typically you would use the Upload Drag Area component instead of this directive. + +### Event handler implementations ```ts export class SomeComponent implements OnInit { @@ -52,23 +71,6 @@ export class SomeComponent implements OnInit { } ``` -### Properties +## See also -| Name | Type | Default | Description | -| --- | --- | --- | --- | -| file-draggable | boolean | true | Toggles between enable/disable of the drag-and-drop functionality. | - -### Events - -| Name | Description | -| --- | --- | -| filesEntityDropped | Emitted when one or more files are dragged and dropped onto the draggable element, such as a `div` | -| folderEntityDropped | Emitted when a directory is dragged and dropped onto the draggable element, such as a `div`| -| filesDropped | Emitted when one or more files are dragged and dropped onto the draggable element, such as a `div`| - -## Details -Typically you would use the Upload Drag Area component instead of this directive. - - - - \ No newline at end of file +- [Upload Drag Area component](upload-drag-area.component.md) diff --git a/docs/file-size.pipe.md b/docs/file-size.pipe.md index f4ab4f975f..2655354d79 100644 --- a/docs/file-size.pipe.md +++ b/docs/file-size.pipe.md @@ -16,8 +16,3 @@ The pipe chooses the largest unit that is less than the total number of bytes an divides the total by this number. This ensures that the number of units is greater than 1 (eg, you will see "512 Bytes" rather than "0.5KB"). The pipe parameter indicates the number of decimal places to use for the value, defaulting to 2 decimal places. - - - - - \ No newline at end of file diff --git a/docs/file-uploading-dialog.component.md b/docs/file-uploading-dialog.component.md index 4d16b49a30..26f549b6a2 100644 --- a/docs/file-uploading-dialog.component.md +++ b/docs/file-uploading-dialog.component.md @@ -11,19 +11,16 @@ Shows a dialog listing all the files uploaded with the Upload Button or Drag Are ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | position | string | 'right' | Dialog position. Accepted values are 'left' or 'right' | ## Details This component should be used in combination with the [Upload Button component](upload-button.component.md) or the -[Drag Area component](drag-area.component.md). +[Upload Drag Area component](upload-drag-area.component.md). - - ## See also -- [Upload button component](upload-button.component.md) -- [Upload drag area component](upload-drag-area.component.md) - +- [Upload button component](upload-button.component.md) +- [Upload drag area component](upload-drag-area.component.md) diff --git a/docs/filter.model.md b/docs/filter.model.md index de6bd09d70..359e9da90b 100644 --- a/docs/filter.model.md +++ b/docs/filter.model.md @@ -62,11 +62,8 @@ class TaskQueryRequestRepresentationModel implements TaskQueryRequestRepresentat } ``` - - ## See also -- [Process filters component](process-filters.component.md) -- [Task filters component](task-filters.component.md) -- [Apps process service](apps-process.service.md) - \ No newline at end of file +- [Process filters component](process-filters.component.md) +- [Task filters component](task-filters.component.md) +- [Apps process service](apps-process.service.md) diff --git a/docs/folder-actions.service.md b/docs/folder-actions.service.md index 7e16a40ca9..60720e83fb 100644 --- a/docs/folder-actions.service.md +++ b/docs/folder-actions.service.md @@ -72,10 +72,7 @@ export class MyView { You will probably want to set up all your custom actions at the application root level or with a custom application service. - - ## See also -- [Document actions service](document-actions.service.md) -- [Content action component](content-action.component.md) - \ No newline at end of file +- [Document actions service](document-actions.service.md) +- [Content action component](content-action.component.md) diff --git a/docs/folder-create.directive.md b/docs/folder-create.directive.md index 339f1e60b6..a5c7ba7e7e 100644 --- a/docs/folder-create.directive.md +++ b/docs/folder-create.directive.md @@ -1,17 +1,6 @@ # Folder Create directive - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - +Allows folders to be created. ## Basic Usage @@ -30,11 +19,11 @@ ### Properties -| Name | Type | Default | Description | -| ----------------- | ------------------- | --------- | ----------------------------------------------------------------- | -| adf-create-folder | string | '-my-' | Parent folder where the new folder will be located after creation | +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| adf-create-folder | string | '-my-' | Parent folder where the new folder will be located after creation | ## Details 'FolderCreateDirective' directive needs the id of the parent folder where we want the new folder node to be created. If no value is provided, the '-my-' alias is used. -It opens the FolderDialogComponent to receive data for the new folder. If data is valid, on dialog close, it emits folderCreate event. \ No newline at end of file +It opens the FolderDialogComponent to receive data for the new folder. If data is valid, on dialog close, it emits folderCreate event. diff --git a/docs/folder-edit.directive.md b/docs/folder-edit.directive.md index 14f1469fba..e748aa9194 100644 --- a/docs/folder-edit.directive.md +++ b/docs/folder-edit.directive.md @@ -1,17 +1,6 @@ # Folder Edit directive - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - +Allows folders to be edited. ## Basic Usage diff --git a/docs/form-field.component.md b/docs/form-field.component.md index c48a4e34fd..5b712c5be3 100644 --- a/docs/form-field.component.md +++ b/docs/form-field.component.md @@ -1,7 +1,9 @@ # Form field component + A form field in an APS form. ## Basic Usage + All form field editors (aka widgets) on a Form are rendered by means of a `FormFieldComponent` that takes an instance of a `FormFieldModel`: @@ -13,20 +15,23 @@ This component depends on the `FormRenderingService` to map the `FormFieldModel` based on the field type or the metadata information. ### Properties + | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | field | FormFieldModel | | Contains all the necessary data needed to determine what UI Widget to use when rendering the field in the form. You would typically not create this data manually but instead create the form in APS and export it to get to all the `FormFieldModel` definitions. | ## Details + You would typically not use this component directly but instead use the `` component, which under the hood uses `` components to render the form fields. See next section for how each field in a form definition is mapped to a form field component (i.e. UI widget) implementation. ### Field Type -> Form Field Component mappings + Forms defined in APS have the following default mappings for the form fields: | APS Form Designer Widget | Field Type | Component Type | -| --- | --- | --- | +| ------------------------ | ---------- | -------------- | | Text | text | TextWidgetComponent | | Multi-line text | multi-line-text | MultilineTextWidgetComponentComponent | | Number | integer | NumberWidgetComponent | @@ -46,7 +51,3 @@ Forms defined in APS have the following default mappings for the form fields: | Display text | readonly-text | DisplayTextWidgetComponentComponent | | N/A | container | ContainerWidgetComponent (layout component) | | N/A | N/A | UnknownWidgetComponent | - - - - \ No newline at end of file diff --git a/docs/form-field.model.md b/docs/form-field.model.md index 621fd765fa..785de085fe 100644 --- a/docs/form-field.model.md +++ b/docs/form-field.model.md @@ -1,12 +1,11 @@ # Form Field model -Contains the value and metadata for a field of an -[ADF Form](form.component.md). +Contains the value and metadata for a field of an [ADF Form](form.component.md). ## Properties | Name | Type | Default | Description | -|--|--|--|--| +| ---- | ---- | ------- | ----------- | | id | string | | Field ID | | name | string | | Field name | | type | string | | Field type (see [Form Rendering service](form-rendering.service.md) for a list of available type strings) | @@ -24,7 +23,7 @@ Contains the value and metadata for a field of an | minValue | string | | Minimum allowed value (eg, for number or date) | | maxValue | string | | Minimum allowed value (eg, for number or date) | | regexPattern | string | | Regular expression that text data should match | -| options | FormFieldOption[] | [] | Option items for a dropdown menu | +| options | FormFieldOption\[] | \[] | Option items for a dropdown menu | | restUrl | string | | URL for a REST call to populate a dropdown menu | | restResponsePath | string | | Path within REST response JSON to the array of dropdown data | | restIdProperty | string | | JSON property name to use for the `id` property of a dropdown item | @@ -41,10 +40,10 @@ Contains the value and metadata for a field of an | currency | string | null | Currency symbol for Amount widgets | | dateDisplayFormat | string | | Date/time display format template | | numberOfColumns | number | 1 | Number of columns defined by a container field | -| fields | FormFieldModel[] | [] | Fields contained within a container field | -| columns | ContainerColumnModel[] | [] | Column definitions for a container field | +| fields | FormFieldModel\[] | \[] | Fields contained within a container field | +| columns | ContainerColumnModel\[] | \[] | Column definitions for a container field | | emptyOption | FormFieldOption | | Dropdown menu item to use when no option is chosen | -| validationSummary | string | | Error/information message added during field validation (see [FormFieldValidator](FormFieldValidator) interface) | +| validationSummary | string | | Error/information message added during field validation (see [FormFieldValidator](FormFieldValidator.md) interface) | ## Details @@ -74,26 +73,23 @@ for the user to read. See the [FormFieldValidator](FormFieldValidator.md) page f You can set the items shown on a dropdown menu using data returned by a REST call. The properties used by the call are: -- `restUrl`: The URL for the REST service -- `restResponsePath`: Optional path to an array within the JSON object returned by -the REST call. Each element in the array corresponds to an item on the dropdown. -- `restIdProperty`: The name of a JSON property present in each element of the array -selected by `restResponsePath`. Its value will be used for the `id` property of the -dropdown item. -`restLabelProperty`: The name of a JSON property present in each element of the array -selected by `restResponsePath`. Its value will be used for the `label` property of the -dropdown item (ie, the text visible to the user). +- `restUrl`: The URL for the REST service +- `restResponsePath`: Optional path to an array within the JSON object returned by + the REST call. Each element in the array corresponds to an item on the dropdown. +- `restIdProperty`: The name of a JSON property present in each element of the array + selected by `restResponsePath`. Its value will be used for the `id` property of the + dropdown item. + `restLabelProperty`: The name of a JSON property present in each element of the array + selected by `restResponsePath`. Its value will be used for the `label` property of the + dropdown item (ie, the text visible to the user). The [REST Call Task 101](https://community.alfresco.com/community/bpm/blog/2016/08/31/rest-integration-101) tutorial on the [APS community site](https://community.alfresco.com/community/bpm) contains full details about how the REST calls work, along with a worked example. - - ## See also -- [Extensibility](extensibility.md) -- [FormFieldValidator](FormFieldValidator.md) -- [Form rendering service](form-rendering.service.md) -- [Form component](form.component.md) - \ No newline at end of file +- [Extensibility](extensibility.md) +- [FormFieldValidator](FormFieldValidator.md) +- [Form rendering service](form-rendering.service.md) +- [Form component](form.component.md) diff --git a/docs/form-list.component.md b/docs/form-list.component.md index 4a2c814c0e..23783412e1 100644 --- a/docs/form-list.component.md +++ b/docs/form-list.component.md @@ -1,17 +1,6 @@ # ADF Form List Component -The component shows the activiti forms as a list. - - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - - - - +Shows APS forms as a list. ## Basic Usage @@ -26,5 +15,5 @@ The component shows the activiti forms as a list. The recommended set of properties can be found in the following table: | Name | Type | Default | Description | -| --- | --- | --- | --- | -| forms | any | | The array that contains the information to show inside the list. | \ No newline at end of file +| ---- | ---- | ------- | ----------- | +| forms | any | | The array that contains the information to show inside the list. | diff --git a/docs/form-rendering.service.md b/docs/form-rendering.service.md index 7dbb016006..fa2f47662b 100644 --- a/docs/form-rendering.service.md +++ b/docs/form-rendering.service.md @@ -47,7 +47,7 @@ formRenderingService.setComponentTypeResolver('text', customResolver, true); The Form Rendering service is initialized with the mapping shown in the table below: | Stencil name | Field type string | Component type | -|---|---|---| +| ------------ | ----------------- | -------------- | | Amount | "amount" | AmountWidgetComponent | | Attach | "upload" | AttachWidgetComponent or UploadWidgetComponent (based on metadata) | | Checkbox | "boolean" | CheckboxWidgetComponent | @@ -84,14 +84,8 @@ implements a modified layout or responds differently when the data is entered. S [Form Extensibility and Customisation](extensibility.md) guide for further details and examples of this technique. - - ## See also -- [Extensibility](extensibility.md) -- [Form field model](form-field.model.md) -- [Form component](form.component.md) - - - - +- [Extensibility](extensibility.md) +- [Form field model](form-field.model.md) +- [Form component](form.component.md) diff --git a/docs/form.component.md b/docs/form.component.md index 14abeac0ef..4110767a74 100644 --- a/docs/form.component.md +++ b/docs/form.component.md @@ -1,28 +1,26 @@ -# Activiti Form component +# Form component -The component shows a Form from Activiti (see it live: [Form Quickstart](https://embed.plnkr.co/YSLXTqb3DtMhVJSqXKkE/)) +Shows a Form from APS (see it live: [Form Quickstart](https://embed.plnkr.co/YSLXTqb3DtMhVJSqXKkE/)) - +## Contents - +- [Basic Usage](#basic-usage) -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Advanced properties](#advanced-properties) - * [Events](#events) -- [Details](#details) - * [Custom empty form template](#custom-empty-form-template) - * [Controlling outcome execution behaviour](#controlling-outcome-execution-behaviour) - * [Field Validators](#field-validators) -- [Other documentation](#other-documentation) - * [Common scenarios](#common-scenarios) - + [Changing field value based on another field](#changing-field-value-based-on-another-field) - + [Listen all form Events](#listen-all-form-events) -- [See also](#see-also) + - [Properties](#properties) + - [Advanced properties](#advanced-properties) + - [Events](#events) - +- [Details](#details) - + - [Custom empty form template](#custom-empty-form-template) + - [Controlling outcome execution behaviour](#controlling-outcome-execution-behaviour) + - [Field Validators](#field-validators) + +- [Other documentation](#other-documentation) + + - [Common scenarios](#common-scenarios) + +- [See also](#see-also) ## Basic Usage @@ -104,11 +102,11 @@ and store the form field as metadata. The param nameNode is optional. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| taskId | string | | Task id to fetch corresponding form and values. | -| formId | string | | The id of the form definition to load and display with custom values. | -| formName | string | | Name of the form definition to load and display with custom values. | -| data | FormValues | | Custom form values map to be used with the rendered form. | +| ---- | ---- | ------- | ----------- | +| taskId | string | | Task id to fetch corresponding form and values. | +| formId | string | | The id of the form definition to load and display with custom values. | +| formName | string | | Name of the form definition to load and display with custom values. | +| data | FormValues | | Custom form values map to be used with the rendered form. | | showTitle | boolean | true | Toggle rendering of the form title. | | showCompleteButton | boolean | true | Toggle rendering of the `Complete` outcome button. | | disableCompleteButton | boolean | false | The `Complete` outcome button is shown but it will be disabled. | @@ -118,28 +116,28 @@ and store the form field as metadata. The param nameNode is optional. | showRefreshButton | boolean | true | Toggle rendering of the `Refresh` button. | | showValidationIcon | boolean | true | Toggle rendering of the validation icon next form title. | | saveMetadata | boolean | false | Store the value of the form as metadata. | -| path | string | | Path of the folder where to store the metadata. | +| path | string | | Path of the folder where to store the metadata. | | nameNode | string | true | Name to assign to the new node where the metadata are stored. | -| fieldValidators | FormFieldValidator[] | See [Field Validators](#field-validators) section below | Contains a list of form field validator instances. | +| fieldValidators | FormFieldValidator\[] | See [Field Validators](#field-validators) section below | Contains a list of form field validator instances. | ### Advanced properties The following properties are for complex customisation purposes: | Name | Type | Default | Description | -| --- | --- | --- | --- | -| form | FormModel | | Underlying form model instance. | +| ---- | ---- | ------- | ----------- | +| form | FormModel | | Underlying form model instance. | | showDebugButton | boolean | false | Toggle debug options. | | debugMode | boolean | false | Toggle debug mode, allows displaying additional data for development and debugging purposes. | ### Events -| Name | Return Type | Description | -| --- | --- | --- | +| Name | Return Type | Description | +| ---- | ----------- | ----------- | | formLoaded | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form is loaded or reloaded. | -| formSaved | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form is submitted with `Save` or custom outcomes. | -| formCompleted | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form is submitted with `Complete` outcome. | -| formDataRefreshed | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form values are refreshed due to a data property change | +| formSaved | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form is submitted with `Save` or custom outcomes. | +| formCompleted | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form is submitted with `Complete` outcome. | +| formDataRefreshed | [FormModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form.model.ts) | Invoked when form values are refreshed due to a data property change | | executeOutcome | [FormOutcomeEvent](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-form/src/components/widgets/core/form-outcome-event.model.ts) | Invoked when any outcome is executed, default behaviour can be prevented via `event.preventDefault()` | | error | any | Invoked at any error | @@ -176,7 +174,6 @@ You can add a template that will be show if no form definition has been found

- ``` ### Controlling outcome execution behaviour @@ -225,8 +222,8 @@ export class MyView { There are two additional functions that can be of a great value when controlling outcomes: -- `saveTaskForm()` - saves current form -- `completeTaskForm(outcome?: string)` - save and complete form with a given outcome name +- `saveTaskForm()` - saves current form +- `completeTaskForm(outcome?: string)` - save and complete form with a given outcome name **Please note that if `event.preventDefault()` is not called then default outcome behaviour will also be executed after your custom code.** @@ -278,13 +275,10 @@ formService.formEvents.subscribe((event: Event) => { }); ``` - - ## See also -- [Stencils](stencils.md) -- [FormFieldValidator](FormFieldValidator.md) -- [Extensibility](extensibility.md) -- [Form rendering service](form-rendering.service.md) -- [Form field model](form-field.model.md) - \ No newline at end of file +- [Stencils](stencils.md) +- [FormFieldValidator](FormFieldValidator.md) +- [Extensibility](extensibility.md) +- [Form rendering service](form-rendering.service.md) +- [Form field model](form-field.model.md) diff --git a/docs/form.service.md b/docs/form.service.md index ac5c9bae4f..429ca737ad 100644 --- a/docs/form.service.md +++ b/docs/form.service.md @@ -32,40 +32,40 @@ class MyComponent { ### Events | Name | Args Type | Description | -| --- | --- | --- | +| ---- | --------- | ----------- | | formLoaded | FormEvent | Raised when form has been loaded or reloaded | | formFieldValueChanged | FormFieldEvent | Raised when input values change | | taskCompleted | FormEvent | Raised when a task is completed successfully | -| taskCompletedError | FormErrorEvent | Raised when a task is completed unsuccessfully | +| taskCompletedError | FormErrorEvent | Raised when a task is completed unsuccessfully | | taskSaved | FormEvent | Raised when a task is saved successfully | | taskSavedError | FormErrorEvent | Raised when a task is saved unsuccessfully | | executeOutcome | FormOutcomeEvent | Raised when a form outcome is executed | | formEvents | Event | You can subscribe to this event to listen : ( click, blur, change, focus, focusin, focusout, input, invalid, select) of any elements in the form , see doc below | | validateForm | ValidateFormEvent | Raised each time a form is validated. You can use it to provide custom validation or prevent default behaviour. | -| validateFormField | ValidateFormFieldEvent | Raised each time a form field is validated. You can use it to provide custom validation or prevent default behaviour.| +| validateFormField | ValidateFormFieldEvent | Raised each time a form field is validated. You can use it to provide custom validation or prevent default behaviour. | ### Methods | Name | Params | Returns | Description | -| --- | --- | --- | --- | -| createFormFromANode | (formName: string) | Observable\ | Create a Form with a fields for each metadata properties | -| createForm | (formName: string) | Observable\ | Create a Form | -| addFieldsToAForm | (formId: string, formModel: FormDefinitionModel) | Observable\ | Add Fileds to A form | -| searchFrom | (name: string) | Observable\ | Search For A Form by name | -| getForms | n/a | Observable\ | Get All the forms | -| getProcessDefinitions | n/a | Observable\ | Get Process Definitions | -| getTasks | n/a | Observable\ | Get All the Tasks | -| getTask | (taskId: string) | Observable\ | Get Task | -| saveTaskForm | (taskId: string, formValues: FormValues) | Observable\ | Save Task Form | -| completeTaskForm | (taskId: string, formValues: FormValues, outcome?: string) | Observable\ | Complete Task Form | -| getTaskForm | (taskId: string) | Observable\ | Get Form related to a taskId | -| getFormDefinitionById | (formId: string) | Observable\ | Get Form Definition | -| getFormDefinitionByName | (name: string) | Observable\ | Returns form definition by a given name. | -| getStartFormInstance | (processId: string) | Observable\ | Get start form instance for a given processId | -| getStartFormDefinition | (processId: string) | Observable\ | Get start form definition for a given process | -| getRestFieldValues | (taskId: string, field: string) | Observable\ | | -| getRestFieldValuesByProcessId | (processDefinitionId: string, field: string) | Observable\ | | -| getRestFieldValuesColumnByProcessId | (processDefinitionId: string, field: string, column?: string) | Observable\ | | -| getRestFieldValuesColumn | (taskId: string, field: string, column?: string) | Observable\ | | -| getWorkflowGroups\ | (filter: string, groupId?: string) | Observable\ | | -| getWorkflowUsers\ | (filter: string, groupId?: string) | Observable\ | | +| ---- | ------ | ------- | ----------- | +| createFormFromANode | (formName: string) | Observable\\<any> | Create a Form with a fields for each metadata properties | +| createForm | (formName: string) | Observable\\<any> | Create a Form | +| addFieldsToAForm | (formId: string, formModel: FormDefinitionModel) | Observable\\<any> | Add Fileds to A form | +| searchFrom | (name: string) | Observable\\<any> | Search For A Form by name | +| getForms | n/a | Observable\\<any> | Get All the forms | +| getProcessDefinitions | n/a | Observable\\<any> | Get Process Definitions | +| getTasks | n/a | Observable\\<any> | Get All the Tasks | +| getTask | (taskId: string) | Observable\\<any> | Get Task | +| saveTaskForm | (taskId: string, formValues: FormValues) | Observable\\<any> | Save Task Form | +| completeTaskForm | (taskId: string, formValues: FormValues, outcome?: string) | Observable\\<any> | Complete Task Form | +| getTaskForm | (taskId: string) | Observable\\<any> | Get Form related to a taskId | +| getFormDefinitionById | (formId: string) | Observable\\<any> | Get Form Definition | +| getFormDefinitionByName | (name: string) | Observable\\<any> | Returns form definition by a given name. | +| getStartFormInstance | (processId: string) | Observable\\<any> | Get start form instance for a given processId | +| getStartFormDefinition | (processId: string) | Observable\\<any> | Get start form definition for a given process | +| getRestFieldValues | (taskId: string, field: string) | Observable\\<any> | | +| getRestFieldValuesByProcessId | (processDefinitionId: string, field: string) | Observable\\<any> | | +| getRestFieldValuesColumnByProcessId | (processDefinitionId: string, field: string, column?: string) | Observable\\<any> | | +| getRestFieldValuesColumn | (taskId: string, field: string, column?: string) | Observable\\<any> | | +| getWorkflowGroups\\ | (filter: string, groupId?: string) | Observable\\<GroupModel\[]> | | +| getWorkflowUsers\\ | (filter: string, groupId?: string) | Observable\\<GroupUserModel\[]> | | diff --git a/docs/highlight-transform.service.md b/docs/highlight-transform.service.md index 31ccd68aed..7579dd0c8e 100644 --- a/docs/highlight-transform.service.md +++ b/docs/highlight-transform.service.md @@ -13,10 +13,10 @@ A typical use case for this service is to display the results from a search engi An excerpt of a retrieved document can be shown with the matching search terms highlighted to indicate where they were found. -The service works by adding HTML <span> elements around all sections of text +The service works by adding HTML <span> elements around all sections of text that match the `search` string. You can specify multiple search strings at once by separating them with spaces, so passing "Apple Banana Cherry" in `search` will -highlight any of those words individually. The <span> element includes a +highlight any of those words individually. The <span> element includes a `class` attribute which defaults to "highlight" but you can pass any class name you like using the `wrapperClass` parameter. @@ -33,13 +33,7 @@ interface HightlightTransformResult { The `changed` flag will be false if the search string was not found (ie, no highlighting took place) and true otherwise. - - ## See also -- [Text highlight pipe](text-highlight.pipe.md) -- [Highlight directive](highlight.directive.md) - - - - +- [Text highlight pipe](text-highlight.pipe.md) +- [Highlight directive](highlight.directive.md) diff --git a/docs/highlight.directive.md b/docs/highlight.directive.md index 4d5acef679..210bfcb187 100644 --- a/docs/highlight.directive.md +++ b/docs/highlight.directive.md @@ -35,8 +35,8 @@ If the search string contain spaces then each section between the spaces will be treated as a separate item to highlight. For example, you could use this to highlight all occurrences of words in a list. -The highlighting works by adding an HTML <span> element around the -selected text. The <span> includes a CSS class; this defaults to +The highlighting works by adding an HTML <span> element around the +selected text. The <span> includes a CSS class; this defaults to "adf-highlight" but you can supply your own class using the `adf-highlight-class` property: @@ -51,10 +51,7 @@ property:
``` - - ## See also -- [Text highlight pipe](text-highlight.pipe.md) -- [Highlight transform service](highlight-transform.service.md) - \ No newline at end of file +- [Text highlight pipe](text-highlight.pipe.md) +- [Highlight transform service](highlight-transform.service.md) diff --git a/docs/host-settings.component.md b/docs/host-settings.component.md index 59149c96c5..15c6c5d9ad 100644 --- a/docs/host-settings.component.md +++ b/docs/host-settings.component.md @@ -1,11 +1,10 @@ # Host settings component -This component is a form that allows you to validate and set the url of your content service and process service saving -it in the user local storage +Validates the URLs for ACS and APS and saves them in the user's local storage ![Host settings](docassets/images/host-settings-component.png) -## Basic Usage +## Basic Usage ```html @@ -15,13 +14,11 @@ it in the user local storage ### Properties | Name | Type | Default Value | Description | -| --- | --- | --- | --- | -| providers | string | ALL | Possible valid values are ECM, BPM or ALL. It indicate which URL configurations show | - +| ---- | ---- | ------------- | ----------- | +| providers | string | ALL | Possible valid values are ECM, BPM or ALL. It indicate which URL configurations show | ### Events | Name | Returned Type | Description | -| --- | --- | --- | -| error | string | emitted when the url inserted is wrong | - +| ---- | ------------- | ----------- | +| error | string | emitted when the url inserted is wrong | diff --git a/docs/infinite-pagination.component.md b/docs/infinite-pagination.component.md index 6a5f85954d..0968cdb98b 100644 --- a/docs/infinite-pagination.component.md +++ b/docs/infinite-pagination.component.md @@ -14,18 +14,31 @@ Adds "infinite" pagination to the component it is used with. ``` +## Integrating with Document List + +```html + + + + +``` + ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | pagination | Pagination | `InfinitePaginationComponent.DEFAULT_PAGINATION` | Pagination object | | pageSize | number | `InfinitePaginationComponent.DEFAULT_PAGE_SIZE` | Number of items that are added with each "load more" event | | loading | boolean | false | | +| target | PaginatedComponent | | Component that provides custom pagination support | + ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | loadMore | Emitted when the "Load More" button is clicked | ## Details @@ -43,10 +56,7 @@ page within the list to be shown, whether there are more items left to show, etc See the [Pagination component](pagination.component.md) for more information about the alternative "finite" pagination scheme. - - ## See also -- [Document list component](document-list.component.md) -- [Pagination component](pagination.component.md) - +- [Document list component](document-list.component.md) +- [Pagination component](pagination.component.md) diff --git a/docs/info-drawer-layout.component.md b/docs/info-drawer-layout.component.md index 3b561fe3d2..9dbc989ca9 100644 --- a/docs/info-drawer-layout.component.md +++ b/docs/info-drawer-layout.component.md @@ -26,13 +26,10 @@ Displays a sidebar-style information panel. As the name suggests, this is basically just a layout with CSS styling. There are three regions where you can add your own content, as shown in the example: -- info-drawer-title -- info-drawer-buttons -- info-drawer-content +- info-drawer-title +- info-drawer-buttons +- info-drawer-content - - ## See also -- [Info drawer component](info-drawer.component.md) - +- [Info drawer component](info-drawer.component.md) diff --git a/docs/info-drawer.component.md b/docs/info-drawer.component.md index f0082c5e9a..66f33731af 100644 --- a/docs/info-drawer.component.md +++ b/docs/info-drawer.component.md @@ -27,8 +27,9 @@ Displays a sidebar-style information panel with tabs. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | title | string | null | The title of the info drawer | +| selectedIndex | number | 0 | The selected index tab | | currentTab | any | null | The currently active tab | ## Details @@ -37,9 +38,6 @@ This is a variant of the [Info Drawer Layout component](info-drawer-layout.compo You can also customize the three regions (title, buttons and content) as with the Info Drawer Layout component. - - ## See also -- [Info drawer layout component](info-drawer-layout.component.md) - +- [Info drawer layout component](info-drawer-layout.component.md) diff --git a/docs/language-menu.component.md b/docs/language-menu.component.md index be7351bca5..0692d71fec 100644 --- a/docs/language-menu.component.md +++ b/docs/language-menu.component.md @@ -39,6 +39,7 @@ This is how the configuration looks like in the the "app.config.json" } ] ``` + In case no setting is provided, the component shows only the English language. ### Nested Menu language @@ -58,12 +59,9 @@ How to attach an ADF Language Menu as nested menu ``` + ![Nested Language Menu screenshot](docassets/images/languages-menu-nested.png) ### Nested menu details In the previous example we are using the ADF Language Menu as nested menu. - - - - diff --git a/docs/like.component.md b/docs/like.component.md index eedaba6187..ddee210949 100644 --- a/docs/like.component.md +++ b/docs/like.component.md @@ -1,29 +1,28 @@ # ADF Like component +Allows a user to add "likes" to an item. + ![Custom columns](docassets/images/social1.png) ## Basic Usage ```html -``` +``` ### Properties | Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| nodeId | string | | The identifier of a node.| +| --------- | ---- | ------- | ----------- | +| nodeId | string | | The identifier of a node. | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | changeVote | Raised when vote gets changed | - - ## See also -- [Rating component](rating.component.md) -- [Rating service](rating.service.md) - \ No newline at end of file +- [Rating component](rating.component.md) +- [Rating service](rating.service.md) diff --git a/docs/login.component.md b/docs/login.component.md index 4daf2dd036..971835066f 100644 --- a/docs/login.component.md +++ b/docs/login.component.md @@ -4,25 +4,22 @@ Authenticates to Alfresco Content Services and Alfresco Process Services. ![Login component](docassets/images/custom-footer.png) - +## Contents - +- [Basic usage](#basic-usage) -- [Basic usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - * [Handling events](#handling-events) - * [Change footer content](#change-footer-content) - * [Change header content](#change-header-content) - * [Extra content](#extra-content) - * [Custom logo and background](#custom-logo-and-background) - * [Customize Validation rules](#customize-validation-rules) - * [Controlling form submit execution behaviour](#controlling-form-submit-execution-behaviour) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [Handling events](#handling-events) + - [Change footer content](#change-footer-content) + - [Change header content](#change-header-content) + - [Extra content](#extra-content) + - [Custom logo and background](#custom-logo-and-background) + - [Customize Validation rules](#customize-validation-rules) + - [Controlling form submit execution behaviour](#controlling-form-submit-execution-behaviour) ## Basic usage @@ -36,23 +33,23 @@ Authenticates to Alfresco Content Services and Alfresco Process Services. ### Properties | Name | Type | Default Value | Description | -| --- | --- | --- | --- | -| providers | string | | Possible valid values are ECM, BPM or ALL. The default behaviour of this component will log in only in the ECM . If you want to log in in both systems the correct value to use is ALL. | -| successRoute | string | | Route to redirect to upon successful login. | +| ---- | ---- | ------------- | ----------- | +| providers | string | | Possible valid values are ECM, BPM or ALL. The default behaviour of this component will log in only in the ECM . If you want to log in in both systems the correct value to use is ALL. | +| successRoute | string | | Route to redirect to upon successful login. | | disableCsrf | boolean | false | To prevent the CSRF Token from being submitted. Only for Alfresco Process Services call | -| needHelpLink | string | | It will change the url of the NEED HELP link in the footer | -| registerLink | string | | It will change the url of the REGISTER link in the footer | -| logoImageUrl | string | \ | To change the logo image with a customised image | -| copyrightText | string | \ | The copyright text below the login box | -| backgroundImageUrl | string | \ | To change the background image with a customised image | -| fieldsValidation | { [key: string]: any; }, extra?: { [key: string]: any; } | | Use it to customise the validation rules of the login form | +| needHelpLink | string | | It will change the url of the NEED HELP link in the footer | +| registerLink | string | | It will change the url of the REGISTER link in the footer | +| logoImageUrl | string | \\ | To change the logo image with a customised image | +| copyrightText | string | \\ | The copyright text below the login box | +| backgroundImageUrl | string | \\ | To change the background image with a customised image | +| fieldsValidation | { [key: string]: any; }, extra?: { [key: string]: any; } | | Use it to customise the validation rules of the login form | | showRememberMe | boolean | false | Toggle `Remember me` checkbox visibility | | showLoginActions | boolean | false | Toggle extra actions visibility (`Need Help`, `Register`, etc.) | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | Raised when the login is done | | error | Raised when the login fails | | executeSubmit | Raised when the form is submitted | @@ -230,3 +227,7 @@ export class MyCustomLogin { **Please note that if `event.preventDefault()` is not called then default behaviour will also be executed after your custom code.** + +## See Also + +- [Logout directive](logout.directive.md) diff --git a/docs/logout.directive.md b/docs/logout.directive.md index 11951e9cc5..4631813ded 100644 --- a/docs/logout.directive.md +++ b/docs/logout.directive.md @@ -2,25 +2,12 @@ Logs the user out when the decorated element is clicked. - - - - -- [Basic Usage](#basic-usage) - - - - - ## Basic Usage ```html ``` - - ## See also -- [Login component](login.component.md) - \ No newline at end of file +- [Login component](login.component.md) diff --git a/docs/mime-type-icon.pipe.md b/docs/mime-type-icon.pipe.md index f4dc71e44c..ffd0a05a4a 100644 --- a/docs/mime-type-icon.pipe.md +++ b/docs/mime-type-icon.pipe.md @@ -15,9 +15,6 @@ Retrieves an icon to represent a MIME type. The pipe takes a MIME type as input and returns the URL of an SVG file that symbolizes that type (see the [Thumbnail service](thumbnail.service.md) for the mapping between types and icons). The pipe will return a "miscellaneous" icon when no specific mapping is defined. - - ## See also -- [Thumbnail service](thumbnail.service.md) - \ No newline at end of file +- [Thumbnail service](thumbnail.service.md) diff --git a/docs/node-delete.directive.md b/docs/node-delete.directive.md index 0c72469853..573e6a8937 100644 --- a/docs/node-delete.directive.md +++ b/docs/node-delete.directive.md @@ -19,17 +19,21 @@ Deletes multiple files and folders. ### Properties -| Name | Type | Default | Description | -| ----------------- | ------------------- | ------- | --------------------------- | -| adf-delete | MinimalNodeEntity[] | [] | Nodes to delete | -| permanent | boolean | false | Permanent delete | +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| adf-delete | MinimalNodeEntity\[] | \[] | Nodes to delete | +| permanent | boolean | false | Permanent delete | ### Events -| Name | Description | -| ------------------------- | -------------------------------------------- | -| delete | emitted when delete process is done | +| Name | Description | +| ---- | ----------- | +| delete | emitted when delete process is done | ## Details See **Demo Shell** + +## See also + +- [Node Restore directive](node-restore.directive.md) diff --git a/docs/node-name-tooltip.pipe.md b/docs/node-name-tooltip.pipe.md index 346a73dac8..df16126f82 100644 --- a/docs/node-name-tooltip.pipe.md +++ b/docs/node-name-tooltip.pipe.md @@ -1,21 +1,6 @@ # Node Name Tooltip directive -Formats the tooltip of the underlying Node based on the following rules: - -* if the *title* and *description* are missing, then the tooltip shows the *name*; -* if the *title* is missing, then the tooltip shows the *name* and *description*; -* if the *description* is missing, then the tooltip shows the *name* and *title*; -* if *name* and *title*, *name* and *description*, or *title* and *description* are the same, then only a single line is displayed. - - - - - -- [Basic Usage](#basic-usage) - - - - +Formats the tooltip for a Node. ## Basic Usage @@ -29,9 +14,11 @@ Formats the tooltip of the underlying Node based on the following rules: ``` - - +## Details -## See also +The tooltip is formatted according to the following rules: - \ No newline at end of file +- if the _title_ and _description_ are missing, then the tooltip shows the _name_; +- if the _title_ is missing, then the tooltip shows the _name_ and _description_; +- if the _description_ is missing, then the tooltip shows the _name_ and _title_; +- if _name_ and _title_, _name_ and _description_, or _title_ and _description_ are the same, then only a single line is displayed. diff --git a/docs/node-permission.directive.md b/docs/node-permission.directive.md index 4a799fb373..c65698b8e5 100644 --- a/docs/node-permission.directive.md +++ b/docs/node-permission.directive.md @@ -2,30 +2,14 @@ Selectively disables an HTML element or Angular component - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) -- [Details](#details) - * [HTML element example](#html-element-example) - * [Angular component example](#angular-component-example) - * [Implementing the NodePermissionSubject interface](#implementing-the-nodepermissionsubject-interface) - * [Defining your components as an EXTENDIBLE_COMPONENT parent component](#defining-your-components-as-an-extendible_component-parent-component) - - - - - ## Basic Usage ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| adf-node-permission | [Permissions](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-alfresco-core/src/models/permissions.enum.ts) | null | Node permission to check (create, delete, update, updatePermissions, !create, !delete, !update, !updatePermissions)| -| adf-nodes | MinimalNodeEntity[] | [] | Nodes to check permission for | +| ---- | ---- | ------- | ----------- | +| adf-node-permission | [Permissions](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-alfresco-core/src/models/permissions.enum.ts) | null | Node permission to check (create, delete, update, updatePermissions, !create, !delete, !update, !updatePermissions) | +| adf-nodes | MinimalNodeEntity\[] | \[] | Nodes to check permission for | ## Details @@ -34,8 +18,8 @@ by taking a collection of the `MinimalNodeEntity` instances and checking the par The decorated element will be disabled if: -- there are no nodes in the collection -- at least one of the nodes has no expected permission +- there are no nodes in the collection +- at least one of the nodes has no expected permission ### HTML element example @@ -94,8 +78,8 @@ The directive will look up the component in the dependency injection tree, up to > "The host component is typically the component requesting the dependency. **But when this component is projected into a parent component, that parent component becomes the host.**" -- because of this, you have to provide your component with forward referencing as the EXTENDIBLE_COMPONENT. -- because of the emphasized second sentence you have to provide your component as a viewProvider. +- because of this, you have to provide your component with forward referencing as the EXTENDIBLE_COMPONENT. +- because of the emphasized second sentence you have to provide your component as a viewProvider. ```js import { EXTENDIBLE_COMPONENT } from '@alfresco/adf-core'; diff --git a/docs/node-restore.directive.md b/docs/node-restore.directive.md index 0a6d04f2b5..ba17424c3c 100644 --- a/docs/node-restore.directive.md +++ b/docs/node-restore.directive.md @@ -1,5 +1,7 @@ # Node Restore directive +Restores deleted nodes to their original location. + ## Basic Usage ```html @@ -20,16 +22,16 @@ ### Properties -| Name | Type | Default | Description | -| ----------------- | ------------------- | ------- | ------------------------------- | -| adf-restore | DeletedNodeEntry[] | [] | Deleted nodes to restore | -| location | string | '' | Route path to view restored node | +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| adf-restore | DeletedNodeEntry\[] | \[] | Deleted nodes to restore | +| location | string | '' | Route path to view restored node | ### Events -| Name | Description | -| --------- | ------------------------------- | -| restore | Raised when the restore is done | +| Name | Description | +| ---- | ----------- | +| restore | Raised when the restore is done | ## Details @@ -37,3 +39,7 @@ If the original location doesn't exist anymore, then they remain in the trash list. For single node restore, there is action to jump to the location where the node has been restored and for this `location` is used to specify the route path where the list of nodes are rendered + +## See Also + +- [Node delete directive](node-delete.directive.md) diff --git a/docs/node.service.md b/docs/node.service.md index 13bb79c4a9..7f813f36d0 100644 --- a/docs/node.service.md +++ b/docs/node.service.md @@ -1,5 +1,7 @@ # Node Service -Get Alfresco Repository node metadata and create nodes with metadata. + +Gets Alfresco Repository node metadata and creates nodes with metadata. + This service cannot be used to create nodes with content. ## Importing @@ -16,6 +18,7 @@ export class SomePageComponent implements OnInit { ## Methods #### getNodeMetadata(nodeId: string): Observable`` + Get the metadata and type for passed in node ID (e.g. 3062d73b-fe47-4040-89d2-79efae63869c): ```ts @@ -38,14 +41,12 @@ or the name of the node (i.e. `cm:name`). The `metadata` response looks like in this example: -``` -author: "Martin" -description: "Installation guide for Alfresco 3.3 on Linux" -lastThumbnailModification: "doclib:1505900632400" -title: "Install 3.3 Linux" -versionLabel: "1.0" -versionType: "MAJOR" -``` + author: "Martin" + description: "Installation guide for Alfresco 3.3 on Linux" + lastThumbnailModification: "doclib:1505900632400" + title: "Install 3.3 Linux" + versionLabel: "1.0" + versionType: "MAJOR" Note that the properties are missing namespace prefix. The `nodeType` response will be returned with namespace prefix, such as `cm:content`. @@ -53,6 +54,7 @@ such as `cm:content`. Executing this method on a folder node returns no metadata, just the type. #### createNode(name: string, nodeType: string, properties: any, path: string): Observable`` + Creates a node in the Alfresco Repository with passed in `name`, `nodeType`, and metadata `properties`. It will be created in the folder `path` that is passed in. @@ -70,27 +72,27 @@ this.nodeService2.createNode(nodeName, nodeType, properties, nodePath).subscribe console.log('Error: ', error); }); ``` + Note that the `path` property should not include the **/Company Home** bit. The response includes all metadata about the new node: -``` -entry: - aspectNames: (2) ["cm:titled", "cm:auditable"] - createdAt: Mon Nov 06 2017 13:04:49 GMT+0000 (GMT) {} - createdByUser: {id: "admin@app.activiti.com", displayName: "ADF User"} - id: "1ab71bb1-d67f-4147-95f6-b5801830ca08" - isFile: false - isFolder: true - modifiedAt: Mon Nov 06 2017 13:04:49 GMT+0000 (GMT) {} - modifiedByUser: {id: "admin@app.activiti.com", displayName: "ADF User"} - name: "someFolder" - nodeType: "cm:folder" - parentId: "a29b5fe3-81f6-46a7-9bed-6a53620acb32" - properties: {cm:title: "Some title", cm:description: "Some description"} -``` + entry: + aspectNames: (2) ["cm:titled", "cm:auditable"] + createdAt: Mon Nov 06 2017 13:04:49 GMT+0000 (GMT) {} + createdByUser: {id: "admin@app.activiti.com", displayName: "ADF User"} + id: "1ab71bb1-d67f-4147-95f6-b5801830ca08" + isFile: false + isFolder: true + modifiedAt: Mon Nov 06 2017 13:04:49 GMT+0000 (GMT) {} + modifiedByUser: {id: "admin@app.activiti.com", displayName: "ADF User"} + name: "someFolder" + nodeType: "cm:folder" + parentId: "a29b5fe3-81f6-46a7-9bed-6a53620acb32" + properties: {cm:title: "Some title", cm:description: "Some description"} #### createNodeMetadata(nodeType: string, nameSpace: any, data: any, path: string, name?: string): Observable`` + This is a convenience method if your property list is missing namespace prefix for property names. The namespace prefix can then be supplied separately and this method will prepend it automatically. This method calls the `createNode` method internally: @@ -112,10 +114,8 @@ this.nodeService2.createNodeMetadata(nodeType, propNamespacePrefix, properties, ``` See the `createNode` method for information about the response object. - - + ## See also -- [Nodes api service](nodes-api.service.md) -- [Deleted nodes api service](deleted-nodes-api.service.md) - \ No newline at end of file +- [Nodes api service](nodes-api.service.md) +- [Deleted nodes api service](deleted-nodes-api.service.md) diff --git a/docs/nodes-api.service.md b/docs/nodes-api.service.md index 1525e89d50..fed1b10f59 100644 --- a/docs/nodes-api.service.md +++ b/docs/nodes-api.service.md @@ -2,22 +2,6 @@ Accesses and manipulates ACS document nodes using their node IDs. - - - - -- [Methods](#methods) -- [Details](#details) - * [Getting node information](#getting-node-information) - * [Getting folder node contents](#getting-folder-node-contents) - * [Creating and updating nodes](#creating-and-updating-nodes) - * [Deleting and restoring nodes](#deleting-and-restoring-nodes) -- [See also](#see-also) - - - - - ## Methods `getNode(nodeId: string, options: any = {}): Observable`
@@ -168,12 +152,9 @@ and pages in the Alfresco JS API for further details and options. Note that you can also use the [Deleted Nodes Api service](deleted-nodes-api.service.md) get a list of all items currently in the trashcan. - - ## See also -- [Document library model](document-library.model.md) -- [Deleted nodes api service](deleted-nodes-api.service.md) -- [Document list component](document-list.component.md) -- [Node service](node.service.md) - +- [Document library model](document-library.model.md) +- [Deleted nodes api service](deleted-nodes-api.service.md) +- [Document list component](document-list.component.md) +- [Node service](node.service.md) diff --git a/docs/notification.service.md b/docs/notification.service.md index 613805eb86..c6284e210f 100644 --- a/docs/notification.service.md +++ b/docs/notification.service.md @@ -2,15 +2,7 @@ Shows a notification message with optional feedback. - - - - -- [Details](#details) - - - - +![Notification Service screenshot](docassets/images/NotiService.png) ## Details diff --git a/docs/page-title.service.md b/docs/page-title.service.md index 1d0de1da8d..a1640d81fd 100644 --- a/docs/page-title.service.md +++ b/docs/page-title.service.md @@ -16,12 +16,6 @@ a result of the form "PageName - AppName" (see is not supplied then just the app name is used; this will default to "Alfresco ADF Application" when no app name set in the config file. - - ## See also -- [App config service](app-config.service.md) - - - - +- [App config service](app-config.service.md) diff --git a/docs/pagination.component.md b/docs/pagination.component.md index 290c778f5a..3aa8f33a6a 100644 --- a/docs/pagination.component.md +++ b/docs/pagination.component.md @@ -4,19 +4,6 @@ Adds pagination to the component it is used with. ![](docassets/images/basic.png) - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - - ## Basic Usage ```html @@ -43,20 +30,20 @@ Adds pagination to the component it is used with. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| pagination | Pagination | | Pagination object | -| supportedPageSizes | Array<number> | [ 25, 50, 100 ] | An array of page sizes | -| target | PaginatedComponent | | Component that provides custom pagination support | +| ---- | ---- | ------- | ----------- | +| pagination | Pagination | | Pagination object | +| supportedPageSizes | Array<number> | [ 25, 50, 100 ] | An array of page sizes | +| target | PaginatedComponent | | Component that provides custom pagination support | ### Events | Name | Type | Description | -| --- | --- | --- | -| change | EventEmitter<PaginationQueryParams> | Triggered for any action in pagination | -| nextPage | EventEmitter<Pagination> | Triggered on next page action | -| prevPage | EventEmitter<Pagination> | Triggered on previous page action | -| changePageSize | EventEmitter<Pagination> | Triggered on page size change action | -| changePageNumber | EventEmitter<Pagination> | Triggered on page change action | +| ---- | ---- | ----------- | +| change | EventEmitter<PaginationQueryParams> | Triggered for any action in pagination | +| nextPage | EventEmitter<Pagination> | Triggered on next page action | +| prevPage | EventEmitter<Pagination> | Triggered on previous page action | +| changePageSize | EventEmitter<Pagination> | Triggered on page size change action | +| changePageNumber | EventEmitter<Pagination> | Triggered on page change action | ## Details @@ -83,4 +70,4 @@ export interface PaginatedComponent { ``` Your component needs to provide a `pagination` subject to allow Pagination component to reflect to changes. -Every time user interacts with the Pagination, it will call the `updatePagination` method and pass the parameters. \ No newline at end of file +Every time user interacts with the Pagination, it will call the `updatePagination` method and pass the parameters. diff --git a/docs/people-content.service.md b/docs/people-content.service.md index 25e92ddbb6..d4d179778b 100644 --- a/docs/people-content.service.md +++ b/docs/people-content.service.md @@ -21,10 +21,7 @@ See the [getPerson](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/PeopleApi.md#getPerson) method in the Alfresco JS API for more information about the REST calls used by this service. - - ## See also -- [People process service](people-process.service.md) -- [Ecm user model](ecm-user.model.md) - \ No newline at end of file +- [People process service](people-process.service.md) +- [Ecm user model](ecm-user.model.md) diff --git a/docs/people-list.component.md b/docs/people-list.component.md index 9226cfdf70..343b46568d 100644 --- a/docs/people-list.component.md +++ b/docs/people-list.component.md @@ -59,22 +59,19 @@ In the component template use the people list component: ``` + Note that the people list component is based on the ` - - \ No newline at end of file diff --git a/docs/people-process.service.md b/docs/people-process.service.md index 4d78bfb4e8..84560abf26 100644 --- a/docs/people-process.service.md +++ b/docs/people-process.service.md @@ -19,7 +19,7 @@ Removes a user who is currently involved with a task. ## Details Use `getWorkflowUsers` to find users across all tasks, optionally filtering by the `searchWord` -in the task name. The `taskId` parameter, if used, specifies a task to be *excluded* from the +in the task name. The `taskId` parameter, if used, specifies a task to be _excluded_ from the results. You would typically use this feature to find new users to assign to a task, in which case you would want to exclude users already assigned to that task. @@ -34,12 +34,8 @@ You can find more information about the REST API methods used by this service in (for `getWorkflowUsers`) and the [User API](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-activiti-rest-api/docs/UserApi.md#getuserprofilepictureurl)(for `getUserImage`). - - - ## See also -- [User process model](user-process.model.md) -- [Bpm user model](bpm-user.model.md) -- [People content service](people-content.service.md) - \ No newline at end of file +- [User process model](user-process.model.md) +- [Bpm user model](bpm-user.model.md) +- [People content service](people-content.service.md) diff --git a/docs/people-search.component.md b/docs/people-search.component.md index 1e31cb4f3d..69174c3160 100644 --- a/docs/people-search.component.md +++ b/docs/people-search.component.md @@ -2,19 +2,6 @@ Searches users/people. - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - - ## Basic Usage ```html @@ -24,27 +11,26 @@ Searches users/people. ### Properties | Name | Type | Description | -| --- | --- | --- | -| results | Observable | The params to show people list | +| ---- | ---- | ----------- | +| results | Observable<User\[]> | The params to show people list | ### Events | Name | Description | -| --- | --- | -| searchPeople | Raised when the search people with new keyword | -| success | Raised when select the user and click action button | -| closeSearch | Raised when click the clse button | +| ---- | ----------- | +| searchPeople | Raised when the search people with new keyword | +| success | Raised when select the user and click action button | +| closeSearch | Raised when click the clse button | ## Details - ```html +```html - {{ 'TASK_DETAILS.LABELS.ADD_PEOPLE' | translate }} - {{ 'PEOPLE.ADD_USER' | translate }} - - + (searchPeople)="searchUser($event)" + (success)="involveUser($event)" + (closeSearch)="onCloseSearch()" + [results]="peopleSearch$"> + {{ 'TASK_DETAILS.LABELS.ADD_PEOPLE' | translate }} + {{ 'PEOPLE.ADD_USER' | translate }} + ``` diff --git a/docs/people.component.md b/docs/people.component.md index cb26fc9c0a..88099e667d 100644 --- a/docs/people.component.md +++ b/docs/people.component.md @@ -1,24 +1,21 @@ -# Task People Component +# People Component -Displays involved users to a specified task +Displays users involved with a specified task - +## Contents - +- [Basic Usage](#basic-usage) -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - * [How to customize the people component behavior](#how-to-customize-the-people-component-behavior) - * [Involve People single click and close search](#involve-people-single-click-and-close-search) - * [Involve People single click without close search](#involve-people-single-click-without-close-search) - * [Involve People double click and close search](#involve-people-double-click-and-close-search) - * [Involve People double double without close search](#involve-people-double-double-without-close-search) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [How to customize the people component behavior](#how-to-customize-the-people-component-behavior) + - [Involve People single click and close search](#involve-people-single-click-and-close-search) + - [Involve People single click without close search](#involve-people-single-click-without-close-search) + - [Involve People double click and close search](#involve-people-double-click-and-close-search) + - [Involve People double double without close search](#involve-people-double-double-without-close-search) ## Basic Usage @@ -35,8 +32,8 @@ Displays involved users to a specified task ### Properties | Name | Type | Description | -| --- | --- | --- | -| people | User[] | The array of User object to display | +| ---- | ---- | ----------- | +| people | User\[] | The array of User object to display | | taskId | string | The numeric ID of the task | | readOnly | boolean | The boolean flag | @@ -49,8 +46,9 @@ No Events ### How to customize the people component behavior The people component provide two methods to customize the behavior: -- involveUserAndCloseSearch: The selected user is going to be added and the search section closed -- involveUserWithoutCloseSearch: The selected user is going to be added without close the search section + +- involveUserAndCloseSearch: The selected user is going to be added and the search section closed +- involveUserWithoutCloseSearch: The selected user is going to be added without close the search section In this way will be easy customize the people component to involve the user with the single or double click event: diff --git a/docs/permissions-style.model.md b/docs/permissions-style.model.md index d8315bc80a..fff438ea73 100644 --- a/docs/permissions-style.model.md +++ b/docs/permissions-style.model.md @@ -5,11 +5,11 @@ Sets custom CSS styles for rows of a [Document List](document-list.component.md) ## Properties | Property | Type | Description | -| --- | --- | --- | +| -------- | ---- | ----------- | | isFile | boolean | Does this style apply to files? | | isFolder | boolean | Does this style apply to folders? | -| permission | Permissions | An enum value defining the permissions that this style applies to (see below) | -| css| string | The name of the CSS class to add | +| permission | Permissions | An enum value defining the permissions that this style applies to (see below) | +| css | string | The name of the CSS class to add | ## Details @@ -48,11 +48,9 @@ adf-document-list ::ng-deep adf-datatable tr.document-list__create { If you want to change the style on the folders where the user doesn't have the permission to update: ```ts - let permissionsStyle: PermissionStyleModel[] = []; this.permissionsStyle.push(new PermissionStyleModel('document-list__disable', PermissionsEnum.NOT_UPDATE, false, true)); - ``` ```html @@ -66,9 +64,6 @@ adf-document-list ::ng-deep adf-datatable tr.document-list__disable { } ``` - - ## See also -- [Document list component](document-list.component.md) - \ No newline at end of file +- [Document list component](document-list.component.md) diff --git a/docs/process-attachment-list.component.md b/docs/process-attachment-list.component.md index 01f68571c1..5ad571eb5e 100644 --- a/docs/process-attachment-list.component.md +++ b/docs/process-attachment-list.component.md @@ -2,6 +2,8 @@ Displays attached documents on a specified process instance +![process-attachment-list-sample](docassets/images/process-attachment-list.png) + ## Basic Usage ```html @@ -11,6 +13,10 @@ Displays attached documents on a specified process instance ``` +If the List is empty, a default no content template is displayed. + +![default-no-content-template-sample](docassets/images/default-no-content-template.png) + Make sure to override the UploadService with the ProcessUploadService ```ts @@ -28,6 +34,33 @@ export class MyCustomProcessAttachmentComponent { } ``` +### How to Add Drag and Drop Functionality + +If we want user to be able to upload attachments for empty lists, We can wrap our component with upload drag area component. In that case, We should also pass a custom *no content template* as shown below with our component urging the user to drag files to upload whenever the list is empty. + +```html + + +
//no content template + +
{{This List is empty}}
+
{{Drag and drop to upload}}
+
+
+
+
+
+
+``` + +If the List is empty, the custom no-content template we passed is displayed. + +![custom-no-content-drag-drop-template-sample](docassets/images/custom-no-content-drag-drop-template.png) + ### Properties | Name | Type | Description | diff --git a/docs/process-comments.component.md b/docs/process-comments.component.md index 0f1347bc1e..ea25d1e7e5 100644 --- a/docs/process-comments.component.md +++ b/docs/process-comments.component.md @@ -2,17 +2,6 @@ Displays comments associated with a particular process instance and allows the user to add new comments - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - - - - - ## Basic Usage ```html @@ -24,5 +13,5 @@ Displays comments associated with a particular process instance and allows the u ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | processInstanceId | string | (**required**): The numeric ID of the process instance to display comments for | diff --git a/docs/process-content.service.md b/docs/process-content.service.md index 1944d6c0dd..471031ebbe 100644 --- a/docs/process-content.service.md +++ b/docs/process-content.service.md @@ -1,6 +1,7 @@ # Process Content Service -Manipulate content related to a Process Instance or Task Instance in APS. Related content can be -uploaded to APS via for example a file upload dialog. +Manipulates content related to a Process Instance or Task Instance in APS. + +Related content can be uploaded to APS via for example a file upload dialog. ## Importing diff --git a/docs/process-filter.service.md b/docs/process-filter.service.md index 9ef756cc13..d7bd9dff58 100644 --- a/docs/process-filter.service.md +++ b/docs/process-filter.service.md @@ -1,4 +1,5 @@ # Process Filter Service + Manage Process Filters, which are pre-configured Process Instance queries. ## Importing @@ -15,6 +16,7 @@ export class SomePageComponent implements OnInit { ## Methods #### createDefaultFilters(appId: number): Observable`` + Create and return the default filters for a Process App: ```ts @@ -29,28 +31,27 @@ this.processFilterService.createDefaultFilters(processAppId) The response is an array of `FilterProcessRepresentationModel` objects: -``` -filters: - 0: { - appId: 2 - filter: - name: "" - sort: "created-desc" - state: "running" - icon: "glyphicon-random" - id: null - index: undefined - name: "Running" - recent: true - } - 1: {id: null, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} - 2: {id: null, appId: 2, name: "All", recent: true, icon: "glyphicon-th", …} -``` + filters: + 0: { + appId: 2 + filter: + name: "" + sort: "created-desc" + state: "running" + icon: "glyphicon-random" + id: null + index: undefined + name: "Running" + recent: true + } + 1: {id: null, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} + 2: {id: null, appId: 2, name: "All", recent: true, icon: "glyphicon-th", …} These filters can now be used to get matching process instances for Process App with ID 2, such as 'Running', 'Completed', and 'All' . #### getProcessFilters(appId: number): Observable`` + Get all filters defined for a Process App: ```ts @@ -65,23 +66,23 @@ this.processFilterService.getProcessFilters(processAppId) The response is an array of `FilterProcessRepresentationModel` objects: -``` -filters: - 0: {id: 15, appId: 2, name: "Running", recent: true, icon: "glyphicon-random", …} - 1: {id: 14, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} - 2: {id: 13, appId: 2, name: "All", recent: false, icon: "glyphicon-th", …} - 3: {id: 3003, appId: 2, name: "Running", recent: false, icon: "glyphicon-random", …} - 4: {id: 3004, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} - 5: {id: 3005, appId: 2, name: "All", recent: false, icon: "glyphicon-th", …} - -``` + filters: + 0: {id: 15, appId: 2, name: "Running", recent: true, icon: "glyphicon-random", …} + 1: {id: 14, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} + 2: {id: 13, appId: 2, name: "All", recent: false, icon: "glyphicon-th", …} + 3: {id: 3003, appId: 2, name: "Running", recent: false, icon: "glyphicon-random", …} + 4: {id: 3004, appId: 2, name: "Completed", recent: false, icon: "glyphicon-ok-sign", …} + 5: {id: 3005, appId: 2, name: "All", recent: false, icon: "glyphicon-th", …} + + In this example I had run the `createDefaultFilters` method ones and that created the duplicate of the default filters. These filters can now be used to get matching process instances for Process App with ID 2, such as 'Running', 'Completed', and 'All' . -#### getProcessFilterById(filterId: number, appId?: number): Observable`` +#### getProcessFilterById(filterId: number, appId?: number): Observable`` + Get a specific Process Filter based on its ID, optionally pass in Process App ID to improve performance when searching for filter: @@ -98,18 +99,17 @@ this.processFilterService.getProcessFilterById(filterId, processAppId) The response is a `FilterProcessRepresentationModel` object: -``` -appId: 2 -filter: {sort: "created-desc", name: "", state: "running"} -icon: "glyphicon-random" -id: 3003 -name: "Running" -recent: false -``` + appId: 2 + filter: {sort: "created-desc", name: "", state: "running"} + icon: "glyphicon-random" + id: 3003 + name: "Running" + recent: false The filter can now be used to get 'Running' process instances for Process App with ID 2. -#### getProcessFilterByName(filterName: string, appId?: number): Observable`` +#### getProcessFilterByName(filterName: string, appId?: number): Observable`` + Get a specific Process Filter based on its name, optionally pass in Process App ID to improve performance when searching for filter: @@ -126,20 +126,20 @@ this.processFilterService.getProcessFilterByName(filterName, processAppId) The response is a `FilterProcessRepresentationModel` object: -``` -appId: 2 -filter: {sort: "created-desc", name: "", state: "running"} -icon: "glyphicon-random" -id: 15 -name: "Running" -recent: true -``` + appId: 2 + filter: {sort: "created-desc", name: "", state: "running"} + icon: "glyphicon-random" + id: 15 + name: "Running" + recent: true + If there are several filters with the same name for the Process App, then you get back the first one found matching the name. The filter can now be used to get 'Running' process instances for Process App with ID 2. #### addProcessFilter(filter: FilterProcessRepresentationModel): Observable`` + Add a new Process Instance filter: ```ts @@ -162,13 +162,11 @@ this.processFilterService.addProcessFilter(filterRunningAsc) The response is a `FilterProcessRepresentationModel` object: -``` -appId: 2 -icon: "glyphicon-random" -id: 3008 -name: "RunningAsc" -recent: false -``` + appId: 2 + icon: "glyphicon-random" + id: 3008 + name: "RunningAsc" + recent: false The filter can now be used to get 'Running' process instances for Process App with ID 2 in created date ascending order. @@ -176,6 +174,7 @@ Process App with ID 2 in created date ascending order. See also the `getRunningFilterInstance` method. #### getRunningFilterInstance(appId: number): FilterProcessRepresentationModel + Convenience method to create and return a filter that matches `running` process instances for passed in Process App ID: @@ -187,19 +186,13 @@ console.log('Running filter', runningFilter); The response is a `FilterProcessRepresentationModel` object: -``` -appId: 2 -filter: {sort: "created-desc", name: "", state: "running"} -icon: "glyphicon-random" -id: null -index: undefined -name: "Running" -recent: true -``` + appId: 2 + filter: {sort: "created-desc", name: "", state: "running"} + icon: "glyphicon-random" + id: null + index: undefined + name: "Running" + recent: true The filter can now be used to get 'Running' process instances for Process App with ID 2 in created date ascending order. - - - - \ No newline at end of file diff --git a/docs/process-filters.component.md b/docs/process-filters.component.md index 37283b977c..2802c4046a 100644 --- a/docs/process-filters.component.md +++ b/docs/process-filters.component.md @@ -13,7 +13,7 @@ Collection of criteria used to filter process instances, which may be customized ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | filterParam | [FilterParamsModel](#filterparamsmodel) | The params to filter the task filter. If there is no match the default one (first filter of the list) is selected | | appId | string | Display filters available to the current user for the application with the specified ID. | | appName | string | Display filters available to the current user for the application with the specified name. | @@ -24,7 +24,7 @@ If both `appId` and `appName` are specified then `appName` will take precedence ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | Raised when the list of filters has been successfully loaded from the server | | error | Raised when an error occurs | | filterClick | Raised when the user selects a filter from the list | @@ -39,11 +39,11 @@ process instances are displayed in the list. ### How filter the activiti process filters - ```html +```html + [filterParam]="{index: 0}"> - ``` +``` You can use inside the filterParam one of the properties defined by [FilterParamsModel](#filterparamsmodel) (see below). @@ -58,7 +58,7 @@ You can use inside the filterParam one of the properties defined by [FilterParam ``` | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | id | string | The id of the task filter. | | name | string | The name of the task filter, lowercase is checked. | | index | number | Zero-based position of the filter in the array. | @@ -68,9 +68,6 @@ You can use inside the filterParam one of the properties defined by [FilterParam The process filter often works well as an item in an accordion menu. See the [Accordion component](accordion.component.md) page for an example of how to do set this up. - - ## See also -- [Filter model](filter.model.md) - \ No newline at end of file +- [Filter model](filter.model.md) diff --git a/docs/process-instance-details.component.md b/docs/process-instance-details.component.md index ffb96fb04a..2d8afb75d8 100644 --- a/docs/process-instance-details.component.md +++ b/docs/process-instance-details.component.md @@ -2,18 +2,6 @@ Displays detailed information on a specified process instance - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) - - - - - ## Basic Usage ```html @@ -25,13 +13,13 @@ Displays detailed information on a specified process instance ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | processInstanceId | string | (required): The numeric ID of the process instance to display | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | processCancelledEmitter | Raised when the current process is cancelled by the user from within the component | | taskFormCompletedEmitter | Raised when the form associated with an active task is completed from within the component | -| showProcessDiagram | Raised when the show diagram button is clicked | \ No newline at end of file +| showProcessDiagram | Raised when the show diagram button is clicked | diff --git a/docs/process-instance-header.component.md b/docs/process-instance-header.component.md index 0c3b39db5e..a69a938039 100644 --- a/docs/process-instance-header.component.md +++ b/docs/process-instance-header.component.md @@ -4,18 +4,6 @@ Sub-component of the process details component, which renders some general infor ![adf-process-instance-header](docassets/images/adf-process-instance-header-attachment.png) - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - + [Events](#events) - - - - - ## Basic Usage ```html @@ -26,10 +14,10 @@ Sub-component of the process details component, which renders some general infor ### Properties -| Name | Type| Description | -| --- | --- | --- | +| Name | Type | Description | +| ---- | ---- | ----------- | | processInstance | [ProcessInstanceModel](https://github.com/Alfresco/alfresco-ng2-components/blob/master/ng2-components/ng2-activiti-processlist/src/models/process-instance.model.ts) | (**required**): Full details of the process instance to display information about | #### Events -This component does not define any events. \ No newline at end of file +This component does not define any events. diff --git a/docs/process-instance-tasks.component.md b/docs/process-instance-tasks.component.md index 15aee05e3b..db6b9b3abc 100644 --- a/docs/process-instance-tasks.component.md +++ b/docs/process-instance-tasks.component.md @@ -2,18 +2,6 @@ Lists both the active and completed tasks associated with a particular process instance - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) - - - - - ## Basic Usage ```html @@ -26,12 +14,12 @@ Lists both the active and completed tasks associated with a particular process i ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | processInstanceId | string | (**required**): The ID of the process instance to display tasks for | | showRefreshButton | boolean | (default: `true`): Whether to show a refresh button next to the list of tasks to allow this to be updated from the server | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | taskFormCompletedEmitter | Raised when the form associated with an active task is completed from within the component | diff --git a/docs/process-list.component.md b/docs/process-list.component.md index 03b32be284..51f7b86e79 100644 --- a/docs/process-list.component.md +++ b/docs/process-list.component.md @@ -1,19 +1,6 @@ -# Activiti Process Instance List +# Process Instance List -This component renders a list containing all the process instances matched by the parameters specified. - - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - +Renders a list containing all the process instances matched by the parameters specified. ## Basic Usage @@ -58,28 +45,26 @@ define custom schema in the app.config.json as shown below json format. [presetColumn]="'customSchema'"> ``` + ### Properties -| Name | Description | -| --- | --- | -| appId | The id of the app. | -| processDefinitionKey | The processDefinitionKey of the process. | -| presetColumn | string || The presetColumn of the custom schema to fetch. | -| state | Define state of the processes. Possible values are `running`, `completed` and `all` | -| sort | Define sort of the processes. Possible values are `created-desc`, `created-asc`, `ended-desc`, `ended-asc` | -| schemaColumn | List of columns to display in the process instances datatable (see the [Details](#details) section below) | +| Name | Description | | | +| ---- | ----------- | --- | --- | +| appId | The id of the app. | | | +| processDefinitionKey | The processDefinitionKey of the process. | | | +| presetColumn | string | | The presetColumn of the custom schema to fetch. | +| state | Define state of the processes. Possible values are `running`, `completed` and `all` | | | +| sort | Define sort of the processes. Possible values are `created-desc`, `created-asc`, `ended-desc`, `ended-asc` | | | +| schemaColumn | List of columns to display in the process instances datatable (see the [Details](#details) section below) | | | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | rowClick | Emitted when a row in the process list is clicked | | success | Emitted when the list of process instances has been loaded successfully from the server | | error | Emitted when an error is encountered loading the list of process instances from the server | - - - ## Details Example value for the schemaColumn property (see [Properties](#properties) section above): diff --git a/docs/process.service.md b/docs/process.service.md index bdb6fc8700..7f3b007f9e 100644 --- a/docs/process.service.md +++ b/docs/process.service.md @@ -1,4 +1,5 @@ # Process Service + Manage Process Instances, Process Variables, and Process Audit Log. ## Importing @@ -15,7 +16,8 @@ export class SomePageComponent implements OnInit { ## Methods -#### getProcess(processInstanceId: string): Observable`` +#### getProcess(processInstanceId: string): Observable`` + Get Process Instance metadata for passed in Process Instance ID: ```ts @@ -30,35 +32,34 @@ this.processService.getProcess(processInstanceId).subscribe( (processInstance: P The `processInstanceId` refers to a process instance ID for a running process in APS. The returned `processInstance` object is of type `ProcessInstance` and looks like in this sample: -``` - businessKey: null - ended: null - graphicalNotationDefined: true - id: "11337" - name: "Invoice Approval 2 started from ADF client" - processDefinitionCategory: "http://www.activiti.org/processdef" - processDefinitionDeploymentId: "18" - processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." - processDefinitionId: "InvoiceApprovalProcess:2:21" - processDefinitionKey: "InvoiceApprovalProcess" - processDefinitionName: "Invoice Approval Process" - processDefinitionVersion: 2 - startFormDefined: true - started: Tue Oct 10 2017 10:35:42 GMT+0100 (BST) {} - startedBy: {id: 1, firstName: null, lastName: "Administrator", email: "admin@app.activiti.com"} - suspended: false - tenantId: "tenant_1" - variables: - 0: {name: "initiator", type: "string", value: "1"} - 1: {name: "approver", type: "long", value: 2002} - 2: {name: "companyemail", type: "string", value: "martin.bergljung@xxxxx.com"} - 3: {name: "invoicetobeapproved", value: null} - 4: {name: "invoiceFileName", type: "string", value: " - [Alfresco_Enterprise_Edition_3_3_Windows_Simple_Install.pdf]"} - 5: {name: "comments", value: null} - 6: {name: "form8outcome", type: "string", value: "Reject"} -``` + businessKey: null + ended: null + graphicalNotationDefined: true + id: "11337" + name: "Invoice Approval 2 started from ADF client" + processDefinitionCategory: "http://www.activiti.org/processdef" + processDefinitionDeploymentId: "18" + processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." + processDefinitionId: "InvoiceApprovalProcess:2:21" + processDefinitionKey: "InvoiceApprovalProcess" + processDefinitionName: "Invoice Approval Process" + processDefinitionVersion: 2 + startFormDefined: true + started: Tue Oct 10 2017 10:35:42 GMT+0100 (BST) {} + startedBy: {id: 1, firstName: null, lastName: "Administrator", email: "admin@app.activiti.com"} + suspended: false + tenantId: "tenant_1" + variables: + 0: {name: "initiator", type: "string", value: "1"} + 1: {name: "approver", type: "long", value: 2002} + 2: {name: "companyemail", type: "string", value: "martin.bergljung@xxxxx.com"} + 3: {name: "invoicetobeapproved", value: null} + 4: {name: "invoiceFileName", type: "string", value: " - [Alfresco_Enterprise_Edition_3_3_Windows_Simple_Install.pdf]"} + 5: {name: "comments", value: null} + 6: {name: "form8outcome", type: "string", value: "Reject"} + +#### startProcess(processDefinitionId: string, name: string, outcome?: string, startFormValues?: any, variables?: ProcessInstanceVariable\[]): Observable`` -#### startProcess(processDefinitionId: string, name: string, outcome?: string, startFormValues?: any, variables?: ProcessInstanceVariable[]): Observable`` Start a process based on passed in Process Definition, name, form values or variables. When starting you can choose to pass in form field values or process variables (you cannot pass in both). @@ -84,30 +85,27 @@ this.processService.startProcess(processDefinitionId, name, outcome, startFormVa A `ProcessInstance` object is returned for a successfully started process: -``` -businessKey: null -ended: null -graphicalNotationDefined: true -id: "75001" -name: "Sample Invoice Process" -processDefinitionCategory: "http://www.activiti.org/processdef" -processDefinitionDeploymentId: "18" -processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." -processDefinitionId: "InvoiceApprovalProcess:2:21" -processDefinitionKey: "InvoiceApprovalProcess" -processDefinitionName: "Invoice Approval Process" -processDefinitionVersion: 2 -startFormDefined: false -started: Thu Nov 09 2017 08:15:37 GMT+0000 (GMT) {} -startedBy: {id: 1, firstName: null, lastName: "Administrator", email: "admin@app.activiti.com"} -tenantId: "tenant_1" -variables: - 0: {name: "approver", value: null} - 1: {name: "companyemail", type: "string", value: "someone@acme.com"} - 2: {name: "initiator", type: "string", value: "1"} - 3: {name: "invoicetobeapproved", value: null} - -``` + businessKey: null + ended: null + graphicalNotationDefined: true + id: "75001" + name: "Sample Invoice Process" + processDefinitionCategory: "http://www.activiti.org/processdef" + processDefinitionDeploymentId: "18" + processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." + processDefinitionId: "InvoiceApprovalProcess:2:21" + processDefinitionKey: "InvoiceApprovalProcess" + processDefinitionName: "Invoice Approval Process" + processDefinitionVersion: 2 + startFormDefined: false + started: Thu Nov 09 2017 08:15:37 GMT+0000 (GMT) {} + startedBy: {id: 1, firstName: null, lastName: "Administrator", email: "admin@app.activiti.com"} + tenantId: "tenant_1" + variables: + 0: {name: "approver", value: null} + 1: {name: "companyemail", type: "string", value: "someone@acme.com"} + 2: {name: "initiator", type: "string", value: "1"} + 3: {name: "invoicetobeapproved", value: null} We can see here that the form field values have been converted to process variables. @@ -142,8 +140,9 @@ this.processService.startProcess(processDefinitionId, name) console.log('Error: ', error); }); ``` - + #### cancelProcess(processInstanceId: string): Observable`` + Cancel a process instance by passing in its process instance ID: ```ts @@ -163,7 +162,8 @@ Once a Process Instance is cancelled it will show up under processes with status You typically get the `processInstanceId` when you start a process. It is then contained in the `ProcessInstance.id` response. -#### createOrUpdateProcessInstanceVariables(processDefinitionId: string, variables: ProcessInstanceVariable[]): Observable`` +#### createOrUpdateProcessInstanceVariables(processDefinitionId: string, variables: ProcessInstanceVariable\[]): Observable`` + Create or update variables for a Process Instance: ```ts @@ -182,17 +182,16 @@ this.processService.createOrUpdateProcessInstanceVariables(processInstanceId, va The response is an array of the successfully created `ProcessInstanceVariable`: -``` -Response: - 0: {name: "sampleVar1", type: "string", value: "hello", scope: "global"} - 1: {name: "sampleVar2", type: "string", value: "bye", scope: "global"} -``` + Response: + 0: {name: "sampleVar1", type: "string", value: "hello", scope: "global"} + 1: {name: "sampleVar2", type: "string", value: "bye", scope: "global"} If a variable already exist, then its value will be updated. **Note**. you need to pass in a Process Instance ID here, not a Process Definition ID. #### deleteProcessInstanceVariable(processDefinitionId: string, variableName: string): Observable`` + Delete a variable for a Process Instance: ```ts @@ -207,10 +206,11 @@ this.processService.deleteProcessInstanceVariable(processInstanceId, variableNam ``` The response will be `null` if the variable was successfully deleted from the Process Instance. - + **Note**. you need to pass in a Process Instance ID here, not a Process Definition ID. -#### getProcessInstanceVariables(processDefinitionId: string): Observable`` +#### getProcessInstanceVariables(processDefinitionId: string): Observable`` + Get all the variables for a Process Instance: ```ts @@ -225,20 +225,18 @@ this.processService.getProcessInstanceVariables(processInstanceId) The response is an array of `ProcessInstanceVariable`: -``` -procVars: - 0: {name: "approver", scope: "global", value: null, valueUrl: null} - 1: {name: "companyemail", scope: "global", value: "someone@acme.com", valueUrl: null} - 2: {name: "initiator", scope: "global", value: "1", valueUrl: null} - 3: {name: "sampleVar2", scope: "global", value: "bye", valueUrl: null} - 4: {name: "invoicetobeapproved", scope: "global", value: null, valueUrl: null} - 5: {name: "invoiceFileName", scope: "global", value: " - [UNKNOWN]", valueUrl: null} -``` - + procVars: + 0: {name: "approver", scope: "global", value: null, valueUrl: null} + 1: {name: "companyemail", scope: "global", value: "someone@acme.com", valueUrl: null} + 2: {name: "initiator", scope: "global", value: "1", valueUrl: null} + 3: {name: "sampleVar2", scope: "global", value: "bye", valueUrl: null} + 4: {name: "invoicetobeapproved", scope: "global", value: null, valueUrl: null} + 5: {name: "invoiceFileName", scope: "global", value: " - [UNKNOWN]", valueUrl: null} + **Note**. you need to pass in a Process Instance ID here, not a Process Definition ID. - #### getProcessDefinitions(appId?: number): Observable`` + Get Process Definitions associated with a Process App: ```ts @@ -253,19 +251,17 @@ this.processService.getProcessDefinitions(processAppId) The response is an array of `ProcessDefinitionRepresentation` objects looking like in this example: -``` -0: - category: "http://www.activiti.org/processdef" - deploymentId: "18" - description: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." - hasStartForm: true - id: "InvoiceApprovalProcess:2:21" - key: "InvoiceApprovalProcess" - metaDataValues: [] - name: "Invoice Approval Process" - tenantId: "tenant_1" - version: 2 -``` + 0: + category: "http://www.activiti.org/processdef" + deploymentId: "18" + description: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." + hasStartForm: true + id: "InvoiceApprovalProcess:2:21" + key: "InvoiceApprovalProcess" + metaDataValues: [] + name: "Invoice Approval Process" + tenantId: "tenant_1" + version: 2 If you wanted a list of all available process definitions call the method without specifying the process application ID: @@ -280,6 +276,7 @@ this.processService.getProcessDefinitions() ``` #### getProcessInstances(requestNode: ProcessFilterParamRepresentationModel, processDefinitionKey?: string): Observable`` + Get Process Instances for passed in filter and optionally Process Definition: ```ts @@ -298,21 +295,21 @@ this.processService.getProcessInstances(filterRunningAsc, processDefKey) The response is an array of `ProcessInstance` objects as in this example: -``` - 0: {id: "7501", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:3:2511", tenantId: "tenant_1", …} - 1: {id: "7520", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} - 2: {id: "8206", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} - 3: {id: "8302", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} - 4: {id: "11337", name: "Invoice Approval 2 started from ADF client", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} - 5: {id: "11437", name: "Invoice Approval Process - October 10th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} - 6: {id: "67143", name: "Sample Invoice Approval 2017-10-26", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:5:62514", tenantId: "tenant_1", …} - 7: {id: "75001", name: "Sample Invoice Process", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} -``` + 0: {id: "7501", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:3:2511", tenantId: "tenant_1", …} + 1: {id: "7520", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + 2: {id: "8206", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + 3: {id: "8302", name: "Invoice Approval Process - October 9th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + 4: {id: "11337", name: "Invoice Approval 2 started from ADF client", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + 5: {id: "11437", name: "Invoice Approval Process - October 10th 2017", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + 6: {id: "67143", name: "Sample Invoice Approval 2017-10-26", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:5:62514", tenantId: "tenant_1", …} + 7: {id: "75001", name: "Sample Invoice Process", businessKey: null, processDefinitionId: "InvoiceApprovalProcess:2:21", tenantId: "tenant_1", …} + You can also narrow down the search via other properties in the `ProcessFilterParamRepresentationModel`, such as `processDefinitionId` and `appDefinitionId`. The number of Process Instances that are returned can be controlled with the `page` and `size` properties. #### getProcessTasks(processInstanceId: string, state?: string): Observable`` + Get Task Instances for passed in Process Instance, optionally filter by task state: ```ts @@ -327,46 +324,45 @@ this.processService.getProcessTasks(processInstanceId) The response is an array of `TaskDetailsModel` objects as in this example: -``` -{ - "size":1, - "total":1, - "start":0, - "data":[ - {"id":"75010", - "name":"Approve Invoice - [Invoice 123]", - "description":null, - "category":null, - "assignee":{"id":1,"firstName":null,"lastName":"Administrator","email":"admin@app.activiti.com"}, - "created":"2017-11-09T08:15:37.427+0000", - "dueDate":null, - "endDate":null, - "duration":null, - "priority":50, - "parentTaskId":null, - "parentTaskName":null, - "processInstanceId":"75001", - "processInstanceName":null, - "processDefinitionId":"InvoiceApprovalProcess:2:21", - "processDefinitionName":"Invoice Approval Process", - "processDefinitionDescription":"This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent.", - "processDefinitionKey":"InvoiceApprovalProcess", - "processDefinitionCategory":"http://www.activiti.org/processdef", - "processDefinitionVersion":2, - "processDefinitionDeploymentId":"18", - "formKey":"8", - "processInstanceStartUserId":null, - "initiatorCanCompleteTask":false, - "adhocTaskCanBeReassigned":false, - "taskDefinitionKey":"approveInvoice", - "executionId":"75001", - "memberOfCandidateGroup":false, - "memberOfCandidateUsers":false, - "managerOfCandidateGroup":false - } - ] -} -``` + { + "size":1, + "total":1, + "start":0, + "data":[ + {"id":"75010", + "name":"Approve Invoice - [Invoice 123]", + "description":null, + "category":null, + "assignee":{"id":1,"firstName":null,"lastName":"Administrator","email":"admin@app.activiti.com"}, + "created":"2017-11-09T08:15:37.427+0000", + "dueDate":null, + "endDate":null, + "duration":null, + "priority":50, + "parentTaskId":null, + "parentTaskName":null, + "processInstanceId":"75001", + "processInstanceName":null, + "processDefinitionId":"InvoiceApprovalProcess:2:21", + "processDefinitionName":"Invoice Approval Process", + "processDefinitionDescription":"This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent.", + "processDefinitionKey":"InvoiceApprovalProcess", + "processDefinitionCategory":"http://www.activiti.org/processdef", + "processDefinitionVersion":2, + "processDefinitionDeploymentId":"18", + "formKey":"8", + "processInstanceStartUserId":null, + "initiatorCanCompleteTask":false, + "adhocTaskCanBeReassigned":false, + "taskDefinitionKey":"approveInvoice", + "executionId":"75001", + "memberOfCandidateGroup":false, + "memberOfCandidateUsers":false, + "managerOfCandidateGroup":false + } + ] + } + You can also filter by task state, which can be `active` or `completed`: ```ts @@ -381,6 +377,7 @@ this.processService.getProcessTasks(processInstanceId, taskState) ``` #### fetchProcessAuditJsonById(processId: string): Observable`` + Fetch Process Audit log as JSON for a Process Instance ID: ```ts @@ -395,98 +392,97 @@ this.processService.fetchProcessAuditJsonById(processInstanceId) The response is JSON object with the Process Instance audit log: -``` -{ - "processInstanceId": "75001", - "processInstanceName": "Sample Invoice Process", - "processDefinitionName": "Invoice Approval Process", - "processDefinitionVersion": "2", - "processInstanceStartTime": "Thu Nov 09 08:15:37 GMT 2017", - "processInstanceEndTime": null, - "processInstanceDurationInMillis": null, - "processInstanceInitiator": " Administrator", - "entries": [ { - "index": 1, - "type": "startForm", - "selectedOutcome": null, - "formData": [ + "processInstanceId": "75001", + "processInstanceName": "Sample Invoice Process", + "processDefinitionName": "Invoice Approval Process", + "processDefinitionVersion": "2", + "processInstanceStartTime": "Thu Nov 09 08:15:37 GMT 2017", + "processInstanceEndTime": null, + "processInstanceDurationInMillis": null, + "processInstanceInitiator": " Administrator", + "entries": [ { - "fieldName": "Approver", - "fieldId": "approver", - "value": "" + "index": 1, + "type": "startForm", + "selectedOutcome": null, + "formData": [ + { + "fieldName": "Approver", + "fieldId": "approver", + "value": "" + }, + { + "fieldName": "Company Email", + "fieldId": "companyemail", + "value": "someone@acme.com" + }, + { + "fieldName": "Invoice to be approved", + "fieldId": "invoicetobeapproved", + "value": "" + } + ], + "taskName": null, + "taskAssignee": null, + "activityId": null, + "activityName": null, + "activityType": null, + "startTime": null, + "endTime": null, + "durationInMillis": null }, { - "fieldName": "Company Email", - "fieldId": "companyemail", - "value": "someone@acme.com" + "index": 2, + "type": "activityExecuted", + "selectedOutcome": null, + "formData": [], + "taskName": null, + "taskAssignee": null, + "activityId": "startInvoiceProcess", + "activityName": "Start Invoice Process", + "activityType": "startEvent", + "startTime": "Thu Nov 09 08:15:37 GMT 2017", + "endTime": "Thu Nov 09 08:15:37 GMT 2017", + "durationInMillis": 37 }, { - "fieldName": "Invoice to be approved", - "fieldId": "invoicetobeapproved", - "value": "" + "index": 3, + "type": "activityExecuted", + "selectedOutcome": null, + "formData": [], + "taskName": null, + "taskAssignee": null, + "activityId": "approveInvoice", + "activityName": "Approve Invoice", + "activityType": "userTask", + "startTime": "Thu Nov 09 08:15:37 GMT 2017", + "endTime": null, + "durationInMillis": null + }, + { + "index": 4, + "type": "taskCreated", + "selectedOutcome": null, + "formData": [], + "taskName": "Approve Invoice - [UNKNOWN]", + "taskAssignee": " Administrator", + "activityId": null, + "activityName": null, + "activityType": null, + "startTime": "Thu Nov 09 08:15:37 GMT 2017", + "endTime": null, + "durationInMillis": null } ], - "taskName": null, - "taskAssignee": null, - "activityId": null, - "activityName": null, - "activityType": null, - "startTime": null, - "endTime": null, - "durationInMillis": null - }, - { - "index": 2, - "type": "activityExecuted", - "selectedOutcome": null, - "formData": [], - "taskName": null, - "taskAssignee": null, - "activityId": "startInvoiceProcess", - "activityName": "Start Invoice Process", - "activityType": "startEvent", - "startTime": "Thu Nov 09 08:15:37 GMT 2017", - "endTime": "Thu Nov 09 08:15:37 GMT 2017", - "durationInMillis": 37 - }, - { - "index": 3, - "type": "activityExecuted", - "selectedOutcome": null, - "formData": [], - "taskName": null, - "taskAssignee": null, - "activityId": "approveInvoice", - "activityName": "Approve Invoice", - "activityType": "userTask", - "startTime": "Thu Nov 09 08:15:37 GMT 2017", - "endTime": null, - "durationInMillis": null - }, - { - "index": 4, - "type": "taskCreated", - "selectedOutcome": null, - "formData": [], - "taskName": "Approve Invoice - [UNKNOWN]", - "taskAssignee": " Administrator", - "activityId": null, - "activityName": null, - "activityType": null, - "startTime": "Thu Nov 09 08:15:37 GMT 2017", - "endTime": null, - "durationInMillis": null + "decisionInfo": { + "calculatedValues": [], + "appliedRules": [] + } } - ], - "decisionInfo": { - "calculatedValues": [], - "appliedRules": [] - } -} -``` #### fetchProcessAuditPdfById(processId: string): Observable`` + Fetch Process Audit log as a PDF for a Process Instance ID: ```ts @@ -501,11 +497,4 @@ this.processService.fetchProcessAuditPdfById(processInstanceId) The response is a BLOB as follows: -``` -Process Audit log BLOB: Blob {size: 124511, type: "text/xml"} -``` - - - - - \ No newline at end of file + Process Audit log BLOB: Blob {size: 124511, type: "text/xml"} diff --git a/docs/product-version.model.md b/docs/product-version.model.md index f81eeff0a0..0496409a03 100644 --- a/docs/product-version.model.md +++ b/docs/product-version.model.md @@ -67,9 +67,6 @@ class VersionModuleModel { } ``` - - ## See also -- [Discovery api service](discovery-api.service.md) - \ No newline at end of file +- [Discovery api service](discovery-api.service.md) diff --git a/docs/rating.component.md b/docs/rating.component.md index 0ec88269a5..e1bb0b6534 100644 --- a/docs/rating.component.md +++ b/docs/rating.component.md @@ -1,5 +1,7 @@ # ADF Rating component +Allows a user to add ratings to an item. + ![Rating component screenshot](docassets/images/social2.png) ## Basic Usage @@ -8,24 +10,21 @@ -``` +``` ### Properties | Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| nodeId | string | | The identifier of a node | +| --------- | ---- | ------- | ----------- | +| nodeId | string | | The identifier of a node | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | changeVote | Raised when vote gets changed | - - ## See also -- [Like component](like.component.md) -- [Rating service](rating.service.md) - \ No newline at end of file +- [Like component](like.component.md) +- [Rating service](rating.service.md) diff --git a/docs/rating.service.md b/docs/rating.service.md index 92706b19aa..e81a346db6 100644 --- a/docs/rating.service.md +++ b/docs/rating.service.md @@ -25,10 +25,7 @@ See the [Ratings API](https://github.com/Alfresco/alfresco-js-api/blob/master/sr in the Alfresco JS API for more information about the returned data and the REST API that this service is based on. - - ## See also -- [Like component](like.component.md) -- [Rating component](rating.component.md) - \ No newline at end of file +- [Like component](like.component.md) +- [Rating component](rating.component.md) diff --git a/docs/renditions.service.md b/docs/renditions.service.md index bdca46654a..51e490a7ab 100644 --- a/docs/renditions.service.md +++ b/docs/renditions.service.md @@ -1,6 +1,40 @@ -# Renditions Service +# Renditions service -* getRenditionsListByNodeId(nodeId: string) -* createRendition(nodeId: string, encoding: string) -* getRendition(nodeId: string, encoding: string) -* isRenditionAvailable(nodeId: string, encoding: string) +Manages prearranged conversions of content to different formats. + +## Methods + +`isRenditionAvailable(nodeId: string, encoding: string): Observable`
+Has the specified rendition been set up for this item? + +`isConversionPossible(nodeId: string, encoding: string): Observable`
+Is it possible to convert this item to the specified format? + +`getRenditionUrl(nodeId: string, encoding: string): string`
+Gets a URL linking to a rendition. + +`getRenditionsListByNodeId(nodeId: string): Observable`
+Gets all available renditions for an item. + +`convert(nodeId: string, encoding: string, pollingInterval: number = 1000)`
+Performs a format conversion on an item directly. + +## Details + +ACS allows content items to be converted to other formats for display or delivery. +For example, a raw text file might be converted to HTML to enable better formatting +in a web browser or a PDF might be converted to an equivalent bitmap image. A +*rendition* is a prearranged conversion that is set up for an item for convenient +repeated use. More information about renditions is available in the +[Content Services documentation](https://docs.alfresco.com/5.2/references/dev-extension-points-content-transformer.html). + +In the Renditions service methods, the `nodeId` is a string identifying the content +node that the rendition applies to. This can be obtained from +[Document List component](document-list.component.md) events and various other places +in the ADF API. The `encoding` identifies the conversion performed by the rendition. + +See the +[Renditions API page](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/RenditionsApi.md#createRendition) +in the Alfresco JS API for more information about the +[RenditionPaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/RenditionPaging.md) +class and other implementation details. diff --git a/docs/search-api.service.md b/docs/search-api.service.md index 784c8c3d25..81d4859868 100644 --- a/docs/search-api.service.md +++ b/docs/search-api.service.md @@ -12,8 +12,3 @@ Searches the repository. See the [Alfresco JS API](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-search-rest-api/docs/SearchApi.md#search) for the format of the query and returned data. - - - - - \ No newline at end of file diff --git a/docs/search-configuration.interface.md b/docs/search-configuration.interface.md new file mode 100644 index 0000000000..0bc74c7f42 --- /dev/null +++ b/docs/search-configuration.interface.md @@ -0,0 +1,88 @@ +# Search Configuration interface + +Provides fine control of parameters to a search. + +## Methods + +`generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody`
+Generates a QueryBody object with custom search parameters. + +## Details + +The interface defines a service that generates a custom +[QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) +object. This object can then be supplied to a search operation to refine the search parameters. + +A standard implementation, the +[Search Configuration service](search-configuration.service.md) is provided in the ADF Core library +source. This works fine in most cases but if you need to, you can implement your own service, as +described below. + +### How to use the interface + +1. Implement the service class + + Create your own service class to implement the SearchConfigurationInterface. This defines the + the `generateQueryBody` method that returns the QueryBody object. See the + [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) + page in the Alfresco JS API for further details about the options this object provides. + + An example implementation is given below: + + ```ts + import { QueryBody } from 'alfresco-js-api'; + import { SearchConfigurationInterface } from '@alfresco/adf-core'; + + export class TestSearchConfigurationService implements SearchConfigurationInterface { + + constructor() { + } + + public generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody { + const defaultQueryBody: QueryBody = { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: maxResults, + skipCount: skipCount + }, + filterQueries: [ + { query: "TYPE:'cm:folder'" }, + { query: 'NOT cm:creator:System' }] + }; + + return defaultQueryBody; + } + } + ``` + +2. Provide your service class to the module + + Once you have created your service class, you must inform the component to use it instead + of the default one. This is easily done using the component providers: + + ```ts + import { SearchService, SearchConfigurationService } from '@alfresco/adf-core'; + import { TestSearchConfigurationService } from './search-config-test.service'; + + @Component({ + selector: 'app-search-extended-component', + templateUrl: './search-extended.component.html', + styleUrls: ['./search-extended.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [ + { provide: SearchConfigurationService, useClass: TestSearchConfigurationService }, + SearchService + ] + }) + ``` + + You also need to add the SearchService as a provider to avoid overriding the module instance. This component will have his own instance of the SearchService that will use the class you have provided + as its configuration. + +## See also + +- [Search component](search.component.md) +- [Search configuration service](search-configuration.service.md) diff --git a/docs/search-configuration.service.md b/docs/search-configuration.service.md new file mode 100644 index 0000000000..0cf3f78bae --- /dev/null +++ b/docs/search-configuration.service.md @@ -0,0 +1,26 @@ +# Search Configuration service + +Provides fine control of parameters to a search. + +## Methods + +`generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody`
+Generates a QueryBody object with custom search parameters. + +## Details + +The `generateQueryBody` method returns a +[QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) +object. This configures the search to use `searchTerm` along with `maxResults` and `skipCount` +specified for the paging of the search results. + +This service is a standard implementation of the +[Search configuration interface](search-configuration.interface.md) that works well for many +common cases. However, you can also supply your own implementation if you need to. See the +[Search configuration interface](search-configuration.interface.md) page for full details and +example code. + +## See also + +- [Search component](search.component.md) +- [Search configuration interface](search-configuration.interface.md) diff --git a/docs/search-control.component.md b/docs/search-control.component.md index 8e9fe6c9d1..fa492e3adb 100644 --- a/docs/search-control.component.md +++ b/docs/search-control.component.md @@ -4,20 +4,6 @@ Displays a input text which shows find-as-you-type suggestions. ![adf-search-control](docassets/images/search-control-component.png) - - - - - -- [Basic usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - - ## Basic usage ```html @@ -31,20 +17,20 @@ Displays a input text which shows find-as-you-type suggestions. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| searchTerm | string | | Search term to pre-populate the field with | -| inputType | string | "text" | Type of the input field to render, e.g. "search" or "text" (default) | -| autocomplete | boolean | true | Whether the browser should offer field auto-completion for the input field to the user. | -| highlight | boolean | false | Use the true value if you want to see the searched word highlighted. | -| expandable | boolean | true | Whether to use an expanding search control, if false then a regular input is used. | -| liveSearchEnabled | boolean | true | Whether find-as-you-type suggestions should be offered for matching content items. Set to false to disable. | -| liveSearchMaxResults | number | 5 | Maximum number of results to show in the live search. | -| customQueryBody | [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api | +| ---- | ---- | ------- | ----------- | +| searchTerm | string | | Search term to pre-populate the field with | +| inputType | string | "text" | Type of the input field to render, e.g. "search" or "text" (default) | +| autocomplete | boolean | true | Whether the browser should offer field auto-completion for the input field to the user. | +| highlight | boolean | false | Use the true value if you want to see the searched word highlighted. | +| expandable | boolean | true | Whether to use an expanding search control, if false then a regular input is used. | +| liveSearchEnabled | boolean | true | Whether find-as-you-type suggestions should be offered for matching content items. Set to false to disable. | +| liveSearchMaxResults | number | 5 | Maximum number of results to show in the live search. | +| customQueryBody | [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api. This input is deprecated, to use the extended query body function please refer to the suggested solution [here](search-configuration.interface.md) | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | searchChange | Emitted when the search term is changed. The search term is provided in the 'value' property of the returned object. If the term is less than three characters in length then the term is truncated to an empty string. | | submit | Emitted when the search is submitted pressing ENTER button. The search term is provided as value of the event. | | optionClicked | Emitted when a file item from the list of find-as-you-type results is selected | diff --git a/docs/search.component.md b/docs/search.component.md index 63bb972fdf..e7b8f6b709 100644 --- a/docs/search.component.md +++ b/docs/search.component.md @@ -1,19 +1,6 @@ # Search component - - - - - - -- [Basic usage](#basic-usage) - * [Properties](#properties) - * [Events](#events) -- [Details](#details) - - - - +Searches items for supplied search terms. ## Basic usage @@ -27,22 +14,23 @@ ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| searchTerm | string | | Search term to use when executing the search. Updating this value will run a new search and update the results | -| maxResults | number | 20 | Maximum number of results to show in the search. | -| skipResults | number | 0 | Number of results to skip from the results pagination. | -| displayWith | function | | Function that maps an option's value to its display value in the trigger | -| queryBody| [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api | +| ---- | ---- | ------- | ----------- | +| searchTerm | string | | Search term to use when executing the search. Updating this value will run a new search and update the results | +| maxResults | number | 20 | Maximum number of results to show in the search. | +| skipResults | number | 0 | Number of results to skip from the results pagination. | +| displayWith | function | | Function that maps an option's value to its display value in the trigger | +| queryBody | [QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) | | object which allow you to perform more elaborated query from the search api. This input is deprecated, to use the extended query body function please refer to the suggested solution [here](search-configuration.interface.md) | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | resultLoaded | Emitted when search results have fully loaded | ## Details ### Customise Search Results + You have to add a template that will be shown when the results are loaded. ```html @@ -56,6 +44,7 @@ You have to add a template that will be shown when the results are loaded. ``` + The results are provided via the [$implicit variable of angular2](https://angular.io/api/common/NgTemplateOutlet) and can be accessed via the sugar sintax 'let-yourChosenName'. As per example above the result will be something like : ![adf-search-control](docassets/images/search-component-simple-template.png) @@ -106,9 +95,10 @@ Which will look like : ![adf-search-control](docassets/images/search-component-complex-template.png) - ### Attach an input field to the search + You can also attach your input field to the adf-search component via the trigger [searchAutocomplete] +Yuo can do this by exporting the adf-search panel instance into a local template variable (here we called it "search"), and binding that variable to the input's searchAutocomplete property. ```html @@ -121,4 +111,20 @@ You can also attach your input field to the adf-search component via the trigger ``` -In this way it is possible to fetch the results from the word typed into the input text straight into the adf-search component via the custom template variable. \ No newline at end of file + +In this way it is possible to fetch the results from the word typed into the input text straight into the adf-search component via the custom template variable. + +### Custom search configuration + +You can get finer control over the parameters of a search by defining them in a custom +[QueryBody](https://github.com/Alfresco/alfresco-js-api/blob/1.6.0/src/alfresco-search-rest-api/docs/QueryBody.md) +object. The recommended way to do this is with a custom implementation of the +[Search Configuration interface](search-configuration.interface.md) (the `queryBody` parameter of the `Search component` is now deprecated). The ADF source provides a standard implementation of this +interface, `SearchConfigurationService` that you can use as a base to adapt to your needs. See the +[Search Configuration interface](search-configuration.interface.md) page for full details of how to +customize your search. + +## See Also + +- [Search configuration interface](search-configuration.interface.md) +- [Search configuration service](search-configuration.service.md) diff --git a/docs/select-apps-dialog.component.md b/docs/select-apps-dialog.component.md new file mode 100644 index 0000000000..c2b076c538 --- /dev/null +++ b/docs/select-apps-dialog.component.md @@ -0,0 +1,29 @@ +# Select app Component + +Show all available apps and return the selected app. + +![select-apps-dialog](docassets/images/select-apps-dialog.png) + +## Basic Usage + +```ts +import { SelectAppsDialogComponent } from '@alfresco/adf-process-services'; + +constructor(private dialog: MatDialog) { +} + +startSelectDialog(){ + const dialogRef = this.dialog.open(SelectAppsDialogComponent, { + width: '630px', + panelClass: 'adf-version-manager-dialog' + }); + + dialogRef.afterClosed().subscribe(selectedProcess => { + this.processId = selectedProcess.id; + }); +} +``` + +## See Also + +- [Start process component](start-process.component.md) diff --git a/docs/shared-links-api.service.md b/docs/shared-links-api.service.md index 59fac0115a..e0f980b79e 100644 --- a/docs/shared-links-api.service.md +++ b/docs/shared-links-api.service.md @@ -11,15 +11,10 @@ Gets shared links available to the current user. Content Services allows users to generate URLs that can be shared with other people, even if they don't have a Content Services account. These -URLs are known as *shared links*. +URLs are known as _shared links_. Use `getSharedLinks` to find all the shared links that are available to the current user. You can supply a number of `options` to refine the search; see the [Alfresco JS API](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SharedlinksApi.md#findsharedlinks) for more information. - - - - - \ No newline at end of file diff --git a/docs/sidebar-action-menu.component.md b/docs/sidebar-action-menu.component.md new file mode 100644 index 0000000000..d4bb2cb2a4 --- /dev/null +++ b/docs/sidebar-action-menu.component.md @@ -0,0 +1,38 @@ +# Sidebar action menu component + +Displays a sidebar-action menu information panel. + +![Sidebar action menu button screenshot](docassets/images/sidebar-action-menu-button.png) +![Sidebar action menu icon screenshot](docassets/images/sidebar-action-menu-icon.png) + +## Basic usage + +```html + + arrow_drop_down +
+ queue +
+
+ +
+
+``` + +### Properties + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| title | string | null | The title of the Sidebar action | +| expanded | boolean | false | Toggle the sidebar action menu on expand. +## Details + +As the name suggests, this is basically just a layout with CSS styling. There are three regions where you can add your own content, as shown in the example: + +- sidebar-menu-title-icon +- sidebar-menu-options +- sidebar-menu-expand-icon + diff --git a/docs/site.model.md b/docs/site.model.md index 48b78e2163..4e826d1566 100644 --- a/docs/site.model.md +++ b/docs/site.model.md @@ -40,9 +40,6 @@ class SiteMembersModel { } ``` - - ## See also -- [Sites api service](sites.service.md) - \ No newline at end of file +- [Sites service](sites.service.md) diff --git a/docs/sites-dropdown.component.md b/docs/sites-dropdown.component.md index b82bb12d9e..4011eacacf 100644 --- a/docs/sites-dropdown.component.md +++ b/docs/sites-dropdown.component.md @@ -17,7 +17,7 @@ Displays a dropdown menu to show and interact with the sites of the current user | Attribute | Type | Default | Description | | --- | --- | --- | --- | | hideMyFiles | boolean | false | Hide the "My Files" option added to the list by default | -| siteList | any[] | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. | +| siteList | [SitePaging](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitePaging.md) | null | A custom list of sites to be displayed by the dropdown. If no value is given, the sites of the current user are displayed by default. A list of objects only with properties 'title' and 'guid' is enough to be able to display the dropdown. | | placeholder | string | 'DROPDOWN.PLACEHOLDER_LABEL' | The placeholder text/the key from translation files for the placeholder text to be shown by default| ### Events diff --git a/docs/sites-api.service.md b/docs/sites.service.md similarity index 89% rename from docs/sites-api.service.md rename to docs/sites.service.md index 3b45e94842..38b8b8abc4 100644 --- a/docs/sites-api.service.md +++ b/docs/sites.service.md @@ -40,12 +40,6 @@ and [getSite](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/SitesApi.md#getSite) for more information about the available options. - - ## See also -- [Site model](site.model.md) - - - - +- [Site model](site.model.md) diff --git a/docs/start-form.component.md b/docs/start-form.component.md index f9a16dcbad..a8d1d18f53 100644 --- a/docs/start-form.component.md +++ b/docs/start-form.component.md @@ -16,7 +16,7 @@ Displays the Start Form for a process. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | processDefinitionId | string | | Definition ID of the process to start. | | processId | string | | Process ID of the process to start. | | showOutcomeButtons | boolean | true | Should form outcome buttons be shown? | @@ -26,7 +26,7 @@ Displays the Start Form for a process. ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | outcomeClick | Emitted when the user clicks one of the outcome buttons that completes the form. | ## Details @@ -40,7 +40,6 @@ The `outcomeClick` event is passed a string containing the ID of the outcome but the user clicked. You can pass this value to the `startProcess` method (defined in the [Process service](process.service.md)) when activating the process, if necessary. - - +## See also - \ No newline at end of file +- [Process service](process.service.md) diff --git a/docs/start-process.component.md b/docs/start-process.component.md index 806d4db9c4..334fd6246d 100644 --- a/docs/start-process.component.md +++ b/docs/start-process.component.md @@ -1,6 +1,6 @@ # Start Process component -Displays Start Process, allowing the user to specify some basic details needed to start a new process instance. +Starts a process. ![adf-start-process ](docassets/images/startProcess.png) @@ -8,21 +8,116 @@ Displays Start Process, allowing the user to specify some basic details needed t ```html + [appId]="YOUR_APP_ID"> ``` ### Properties -| Name | Description | -| --- | --- | -| appId | (required): Limit the list of processes which can be started to those contained in the specified app | -| variables | Variables in input to the process [RestVariable](https://github.com/Alfresco/alfresco-js-api/tree/master/src/alfresco-activiti-rest-api/docs/RestVariable.md)| +| Name | Type | Description | +| ---- | -- | ----------- | +| appId | `number` | (optional): Limit the list of processes which can be started to those contained in the specified app | +| name | `string` | (optional) name to assign to the current process | +| processDefinitionName | `string` | (optional) definition name of the process to start | +| variables | `ProcessInstanceVariable[]` |Variables in input to the process [RestVariable](https://github.com/Alfresco/alfresco-js-api/tree/master/src/alfresco-activiti-rest-api/docs/RestVariable.md) | +| values | `FormValues` | Parameter to pass form field values in the start form if is associated | +| showSelectProcessDropdown | `boolean` | hide or show the process selection drodown, true by default | ### Events -| Name | Description | -| --- | --- | -| start | Raised when the process start | -| cancel | Raised when the process canceled | -| error | Raised when the start process fail | +| Name | Type | Description | +| ---- | -- | --------- | +| start | `EventEmitter` | Emitted when the process starts | +| cancel | `EventEmitter` | Emitted when the process is canceled | +| error | `EventEmitter` | Emitted when the start process operation fails | + +### Start a process with a default name and pre-selected process definition name + +```html + + +``` + +If your app has more than one process definition, with the property processDefinitionName you can select as dropdown default value your PROCESS_DEFINITION_NAME. The name will be the PROCESS_NAME. + +### Start a process not included in an app + +```html + + +``` + +With the property processDefinitionName you can select as dropdown default value your PROCESS_DEFINITION_NAME. + + +### Custom data example + +Here is an example of how to pass in form field values to initialize the start form that has been +defined for the process: + +```ts +const formValues: FormValues = { + 'test_1': 'value_1', + 'test_2': 'value_2', + 'test_3': 'value_1', + 'test_4': 'dropdown_id', + 'test_5': 'dropdown_label', + 'dropdown': {'id': 'dropdown_id', 'name': 'dropdown_label'} +}; +``` + +```html + + +``` + +### Attaching a File to the start form of the process + +After the repository is created in APS, you can it in the Alfresco Repositories list. +If the ID is set to 1 then all default values are fine. +However, if it is set to something else, for example, _1002_ and the name is _alfresco_, you must set the property `alfrescoRepositoryName` in your `app.config.json` file to _alfresco-1002_: + +```json + { + name: 'Alfresco ADF Application' + }, + ecmHost: 'http://{hostname}{:port}/ecm', + bpmHost: 'http://{hostname}{:port}/bpm', + logLevel: 'silent', + alfrescoRepositoryName : 'alfresco-1002' +``` + +You then need to pass the node as the input `values` object with the other properties: + +```ts +let node: MinimalNodeEntryEntity = this.nodesApiService.getNode(NODE_ID); + +const formValues: FormValues = { + 'file' : node + 'field_one': 'example text' +}; +``` + +Note that in the object above, the key `file` is the name of the attach file field in the start form of the process. The value of the `file` property must be a +[MinimalNodeEntryEntity](document-library.model.md): + +```html + + +``` + +The result will be the start form prefilled with the file data: + +![Start process load file](docassets/images/start_process.png) + +## See also + +- [Select Apps Dialog component](select-apps-dialog.component.md) diff --git a/docs/storage.service.md b/docs/storage.service.md index 04c14e13d4..ce3cbe3ed2 100644 --- a/docs/storage.service.md +++ b/docs/storage.service.md @@ -32,10 +32,7 @@ more widely supported by browsers and can be set to expire after a certain date. If local storage is not available then non-persistent memory storage within the app is used instead. - - ## See also -- [Cookie service](cookie.service.md) -- [Content service](content.service.md) - \ No newline at end of file +- [Cookie service](cookie.service.md) +- [Content service](content.service.md) diff --git a/docs/summary.json b/docs/summary.json index ba164f88fd..a1683909c0 100644 --- a/docs/summary.json +++ b/docs/summary.json @@ -3,5 +3,5 @@ { "title": "Angular Material Design", "file": "angular-material-design.md" }, { "title": "Theming", "file": "theming.md" }, { "title": "Typography", "file": "typography.md" }, - { "title": "Walkthrough: adding indicators to highlight information about a node", "file": "metadata-indicators.md" } + { "title": "Walkthrough - adding indicators to highlight information about a node", "file": "metadata-indicators.md" } ] diff --git a/docs/tag-actions.component.md b/docs/tag-actions.component.md index 1be68f1399..ad6974f559 100644 --- a/docs/tag-actions.component.md +++ b/docs/tag-actions.component.md @@ -1,4 +1,6 @@ -# Alfresco Tag Node Actions List component +# Tag Node Actions List component + +Shows available actions for tags. ![Custom columns](docassets/images/tag3.png) @@ -8,10 +10,10 @@ -``` +``` ### Properties | Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| nodeId | string | | The identifier of a node | \ No newline at end of file +| --------- | ---- | ------- | ----------- | +| nodeId | string | | The identifier of a node | diff --git a/docs/tag-list.component.md b/docs/tag-list.component.md index 46214d914f..5fb700cdc5 100644 --- a/docs/tag-list.component.md +++ b/docs/tag-list.component.md @@ -1,3 +1,9 @@ -# Alfresco Tag List component +# Tag List component -![Custom columns](docassets/images/tag2.png) \ No newline at end of file +Shows tags for an item. + +![Custom columns](docassets/images/tag2.png) + +## See Also + +- [Tag service](tag.service.md) diff --git a/docs/tag-node-list.component.md b/docs/tag-node-list.component.md index 25ec81dba5..30e4dd4dea 100644 --- a/docs/tag-node-list.component.md +++ b/docs/tag-node-list.component.md @@ -1,4 +1,6 @@ -# Alfresco Tag Node List component +# Tag Node List component + +Shows tags for a node. ![Custom columns](docassets/images/tag1.png) @@ -8,10 +10,10 @@ -``` +``` ### Properties | Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| nodeId | string | | The identifier of a node | +| --------- | ---- | ------- | ----------- | +| nodeId | string | | The identifier of a node | diff --git a/docs/tag.service.md b/docs/tag.service.md index e669188604..5c4478eda7 100644 --- a/docs/tag.service.md +++ b/docs/tag.service.md @@ -35,12 +35,6 @@ in the Alfresco JS API for more information about the types returned by Tag service methods and for the implementation of the REST API the service is based on. - - ## See also -- [Tag list component](tag-list.component.md) - - - - +- [Tag list component](tag-list.component.md) diff --git a/docs/task-details.model.md b/docs/task-details.model.md index 9e3928a6e1..99f5addb01 100644 --- a/docs/task-details.model.md +++ b/docs/task-details.model.md @@ -1,4 +1,8 @@ -# TaskDetailsModel +# Task Details model + +Information about a task. + +## Details ```json { @@ -9,3 +13,7 @@ "description": "string" } ``` + +## See Also + +- [Task header component](task-header.component.md) diff --git a/docs/task-filters.component.md b/docs/task-filters.component.md index 6cc6d1b296..a548d59c78 100644 --- a/docs/task-filters.component.md +++ b/docs/task-filters.component.md @@ -1,4 +1,4 @@ -# Activiti Filter component +# Task Filters component Shows all available filters. @@ -11,7 +11,7 @@ Shows all available filters. ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | filterParam | [FilterParamsModel](#filterparamsmodel) | The params to filter the task filter. If there is no match the default one (first filter of the list) is selected | | appId | string | Display filters available to the current user for the application with the specified ID. | | `appName` | string | Display filters available to the current user for the application with the specified name. | @@ -22,20 +22,19 @@ If both `appId` and `appName` are specified then `appName` will take precedence ### Events | Name | Description | -| --- | --- | -| filterClick | Raised when the filter in the list is clicked | -| success | Raised when the list is loaded | -| error | Raised if there is an error during the loading | +| ---- | ----------- | +| filterClick | Raised when the filter in the list is clicked | +| success | Raised when the list is loaded | +| error | Raised if there is an error during the loading | ## Details ### How filter the activiti task filters - ```html +```html + [filterParam]="{name:'My tasks'}"> - ``` You can use inside the filterParam one of the properties from [FilterParamsModel](#filterparamsmodel) (see below). @@ -51,7 +50,7 @@ You can use inside the filterParam one of the properties from [FilterParamsModel ``` | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | id | string | The id of the task filter | | name | string | The name of the task filter, lowercase is checked | | index | string | The zero-based position of the filter in the array. | @@ -61,9 +60,6 @@ You can use inside the filterParam one of the properties from [FilterParamsModel The task filter often works well as an item in an accordion menu. See the [Accordion component](accordion.component.md) page for an example of how to do set this up. - - ## See also -- [Filter model](filter.model.md) - \ No newline at end of file +- [Filter model](filter.model.md) diff --git a/docs/task-header.component.md b/docs/task-header.component.md index cb0c0c3130..c85607fc73 100644 --- a/docs/task-header.component.md +++ b/docs/task-header.component.md @@ -1,4 +1,4 @@ -# Activiti Task Header component +# Task Header component Shows all the information related to a task. @@ -15,17 +15,21 @@ Shows all the information related to a task. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| taskDetails | [TaskDetailsModel](#taskdetailsmodel) | | (**required**) The task details related to the task. | -| formName | string | | The name of the form. | +| ---- | ---- | ------- | ----------- | +| taskDetails | [TaskDetailsModel](task-details.model.md) | | (**required**) Details related to the task. | +| formName | string | | The name of the form. | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | claim | Raised when the task is claimed. | | unclaim | Raised when the task is unclaimed (requeued). | ## Details The purpose of the component is to populate the local variable called `properties` (array of CardViewModel), with all the information that we want to display. + +## See also + +- [Task Details model](task-details.model.md) diff --git a/docs/task-list.component.md b/docs/task-list.component.md index 152335dba4..368c2ec4f7 100644 --- a/docs/task-list.component.md +++ b/docs/task-list.component.md @@ -1,4 +1,4 @@ -# Activiti Task List component +# Task List component Renders a list containing all the tasks matched by the parameters specified. @@ -12,6 +12,39 @@ Renders a list containing all the tasks matched by the parameters specified. ``` +You can pass schema as data adapter for the tasklist like shown below : + +```ts +let data = new ObjectDataTableAdapter( + // Row data + [ + { id: 1, name: 'Name 1' }, + { id: 2, name: 'Name 2' } + ], + // Column schema + [ + { + type: 'text', + key: 'id', + title: 'Id', + sortable: true + }, + { + type: 'text', + key: 'name', + title: 'Name', + sortable: true + } + ] +); +``` + +```html + + +``` + You can also use HTML-based schema declaration like shown below: ```html @@ -23,9 +56,9 @@ You can also use HTML-based schema declaration like shown below: ``` -You can also use custom schema declaration as shown below: +You can also use static custom schema declaration as shown below: -define custom schema in the app.config.json as shown below json format. +define static custom schema in the app.config.json as shown below json format. ```json "adf-task-list": { @@ -47,38 +80,77 @@ define custom schema in the app.config.json as shown below json format. } } ``` + ```html ``` + +You can also use both HTML-based and app.config.json custom schema declaration at same time like shown below: + +```json +"adf-task-list": { + "presets": { + "customSchema": [ + { + "key": "id", + "type": "text", + "title": "Id", + "sortable": true + }], + "default": [ + { + "key": "name", + "type": "text", + "title": "name", + "sortable": true + }], + } +} +``` + +```html + + + + +
{{getFullName(entry.row.obj.assignee)}}
+
+
+
+
+``` + ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| appId | string || The id of the app. | -| processDefinitionKey | string || The processDefinitionKey of the process. | -| processInstanceId | string || The processInstanceId of the process. | -| presetColumn | string || The presetColumn of the custom schema to fetch. | +| ---- | ---- | ------- | ----------- | +| appId | string | | The id of the app. | +| processDefinitionKey | string | | The processDefinitionKey of the process. | +| processInstanceId | string | | The processInstanceId of the process. | +| presetColumn | string | | The presetColumn of the custom schema to fetch. | | page | number | 0 | The page of the tasks to fetch. | | size | number | 5 | The number of tasks to fetch. | -| assignment | string || The assignment of the process.
    Possible values are:
  • assignee : where the current user is the assignee
  • candidate: where the current user is a task candidate
  • group_x: where the task is assigned to a group where the current user is a member of.
  • no value: where the current user is involved
| -| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | -| multiselect | boolean | false | Toggles multiple row selection, renders checkboxes at the beginning of each row | -| state | string || Define state of the processes. Possible values are: `completed`, `active` | +| assignment | string | | The assignment of the process.
    Possible values are:
  • assignee : where the current user is the assignee
  • candidate: where the current user is a task candidate
  • group_x: where the task is assigned to a group where the current user is a member of.
  • no value: where the current user is involved
| +| selectionMode | string | 'single' | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. | +| multiselect | boolean | false | Toggles multiple row selection, renders checkboxes at the beginning of each row | +| state | string | | Define state of the processes. Possible values are: `completed`, `active` | | hasIcon | boolean | true | Toggle the icon on the left . | -| landingTaskId | string | | Define which task id should be selected after the reloading. If the task id doesn't exist or nothing is passed it will select the first task | -| sort | string | | Define the sort of the processes. Possible values are : `created-desc`, `created-asc`, `due-desc`, `due-asc` | -| data | [DataTableAdapter](DataTableAdapter.md) | | JSON object that represent the number and the type of the columns that you want show (see the [example](#datatableadapter-example) section below) | +| landingTaskId | string | | Define which task id should be selected after the reloading. If the task id doesn't exist or nothing is passed it will select the first task | +| sort | string | | Define the sort of the processes. Possible values are : `created-desc`, `created-asc`, `due-desc`, `due-asc` | +| data | [DataTableAdapter](DataTableAdapter.md) | | JSON object that represent the number and the type of the columns that you want show (see the [example](#datatableadapter-example) section below) | ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | Raised when the task list is loaded | | rowClick | Raised when the task in the list is clicked | -| rowsSelected | Raised when the a row is selected/unselected | +| rowsSelected | Raised when the a row is selected/unselected | ## Details @@ -104,10 +176,7 @@ typical tasklist. You can customize the styling of a column and also add features like tooltips and automatic translation of column titles. See the [DataColumn](data-column.component.md) page for more information about these features. - - ## See also -- [Data column component](data-column.component.md) -- [DataTableAdapter](DataTableAdapter.md) - +- [Data column component](data-column.component.md) +- [DataTableAdapter](DataTableAdapter.md) diff --git a/docs/tasklist.service.md b/docs/tasklist.service.md index b6c79f20d0..1e5f8035ed 100644 --- a/docs/tasklist.service.md +++ b/docs/tasklist.service.md @@ -17,6 +17,7 @@ export class SomePageComponent implements OnInit { ## Methods #### getTaskDetails(taskId: string): Observable`` + Get Task Instance metadata for passed in Task Instance ID: ```ts @@ -31,41 +32,40 @@ this.tasklistService.getTaskDetails(taskInstanceId).subscribe( (taskInstance: Ta The `taskInstanceId` refers to a Task Instance identifier in APS. The returned `taskInstance` object is of type `TaskDetailsModel` and looks like in this sample: -``` -adhocTaskCanBeReassigned: false -assignee: UserProcessModel {pictureId: null, id: 1, email: "admin@app.activiti.com", firstName: null, lastName: "Administrator"} -category: null -created: Wed Oct 11 2017 09:07:14 GMT+0100 (BST) {} -description: null -dueDate: null -duration: null -endDate: null -executionId: "11337" -formKey: "9" -id: "15303" -initiatorCanCompleteTask: false -involvedPeople: [] -managerOfCandidateGroup: false -memberOfCandidateGroup: false -memberOfCandidateUsers: false -name: "Clarify Invoice - Invoice-20302.pdf" -parentTaskId: null -parentTaskName: null -priority: 50 -processDefinitionCategory: "http://www.activiti.org/processdef" -processDefinitionDeploymentId: "18" -processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." -processDefinitionId: "InvoiceApprovalProcess:2:21" -processDefinitionKey: "InvoiceApprovalProcess" -processDefinitionName: "Invoice Approval Process" -processDefinitionVersion: 2 -processInstanceId: "11337" -processInstanceName: null -processInstanceStartUserId: "1" -taskDefinitionKey: "clarifyInvoice" -``` + adhocTaskCanBeReassigned: false + assignee: UserProcessModel {pictureId: null, id: 1, email: "admin@app.activiti.com", firstName: null, lastName: "Administrator"} + category: null + created: Wed Oct 11 2017 09:07:14 GMT+0100 (BST) {} + description: null + dueDate: null + duration: null + endDate: null + executionId: "11337" + formKey: "9" + id: "15303" + initiatorCanCompleteTask: false + involvedPeople: [] + managerOfCandidateGroup: false + memberOfCandidateGroup: false + memberOfCandidateUsers: false + name: "Clarify Invoice - Invoice-20302.pdf" + parentTaskId: null + parentTaskName: null + priority: 50 + processDefinitionCategory: "http://www.activiti.org/processdef" + processDefinitionDeploymentId: "18" + processDefinitionDescription: "This is a simple invoice approval process that allows a person to assign a dedicated approver for the the invoice. It will then be routed to the Accounting department for payment preparation. Once payment is prepared the invoice will be stored in a specific folder and an email notification will be sent." + processDefinitionId: "InvoiceApprovalProcess:2:21" + processDefinitionKey: "InvoiceApprovalProcess" + processDefinitionName: "Invoice Approval Process" + processDefinitionVersion: 2 + processInstanceId: "11337" + processInstanceName: null + processInstanceStartUserId: "1" + taskDefinitionKey: "clarifyInvoice" #### getTaskChecklist(id: string): Observable`` + Get all the sub-task instances for a Task Instance, also called the check list: ```ts @@ -79,48 +79,47 @@ this.tasklistService.getTaskChecklist(parentTaskId).subscribe( (subTasks: TaskDe The response is an array of `TaskDetailsModel` representing the sub-tasks: -``` -Sub Tasks: - 0: - adhocTaskCanBeReassigned: false - assignee: UserProcessModel {pictureId: null, id: 1, email: "admin@app.activiti.com", firstName: null, lastName: "Administrator"} - category: "2" - created: "2017-10-29T07:29:28.881+0000" - description: null - dueDate: null - duration: null - endDate: null - executionId: null - formKey: null - id: "74745" - initiatorCanCompleteTask: false - involvedPeople: undefined - managerOfCandidateGroup: false - memberOfCandidateGroup: false - memberOfCandidateUsers: false - name: "Double check invoice amount" - parentTaskId: "15303" - parentTaskName: "Clarify Invoice - Invoice-10292.pdf" - priority: 50 - processDefinitionCategory: null - processDefinitionDeploymentId: null - processDefinitionDescription: null - processDefinitionId: null - processDefinitionKey: null - processDefinitionName: null - processDefinitionVersion: 0 - processInstanceId: null - processInstanceName: null - processInstanceStartUserId: null - taskDefinitionKey: null - 1 : - {processDefinitionVersion: 0, id: "74746", name: "Verify with the person that did the purchase", priority: 50, assignee: UserProcessModel, …} -``` + Sub Tasks: + 0: + adhocTaskCanBeReassigned: false + assignee: UserProcessModel {pictureId: null, id: 1, email: "admin@app.activiti.com", firstName: null, lastName: "Administrator"} + category: "2" + created: "2017-10-29T07:29:28.881+0000" + description: null + dueDate: null + duration: null + endDate: null + executionId: null + formKey: null + id: "74745" + initiatorCanCompleteTask: false + involvedPeople: undefined + managerOfCandidateGroup: false + memberOfCandidateGroup: false + memberOfCandidateUsers: false + name: "Double check invoice amount" + parentTaskId: "15303" + parentTaskName: "Clarify Invoice - Invoice-10292.pdf" + priority: 50 + processDefinitionCategory: null + processDefinitionDeploymentId: null + processDefinitionDescription: null + processDefinitionId: null + processDefinitionKey: null + processDefinitionName: null + processDefinitionVersion: 0 + processInstanceId: null + processInstanceName: null + processInstanceStartUserId: null + taskDefinitionKey: null + 1 : + {processDefinitionVersion: 0, id: "74746", name: "Verify with the person that did the purchase", priority: 50, assignee: UserProcessModel, …} Looking at the `TaskDetailsModel` for a sub-task we can see that it has a parent task ID that matches what we specified when calling this method. #### getTasks(requestNode: TaskQueryRequestRepresentationModel): Observable`` + Get tasks matching passed in query definition: ```ts @@ -161,22 +160,21 @@ pooled tasks (i.e. tasks that needs to be claimed by a user), then use `candidat A `TaskListModel` object is returned for a successful query and the `data` property is an array of `TaskDetailsModel`: -``` -data: - 0: {id: "75010", name: "Approve Invoice - Invoice-10202.pdf", description: null, category: null, assignee: {…}, …} - 1: {id: "74746", name: "Verify with the person that did the purchase", description: null, category: "2", assignee: {…}, …} - 2: {id: "74745", name: "Double check invoice amount", description: null, category: "2", assignee: {…}, …} - 3: {id: "20686", name: "Sample checklist task 1", description: null, category: "2", assignee: {…}, …} - 4: {id: "15303", name: "Clarify Invoice - Invoice-20302.pdf", description: null, category: null, assignee: {…}, …} -length: 5 -size: 5 -start: 0 -total: 10 -``` + data: + 0: {id: "75010", name: "Approve Invoice - Invoice-10202.pdf", description: null, category: null, assignee: {…}, …} + 1: {id: "74746", name: "Verify with the person that did the purchase", description: null, category: "2", assignee: {…}, …} + 2: {id: "74745", name: "Double check invoice amount", description: null, category: "2", assignee: {…}, …} + 3: {id: "20686", name: "Sample checklist task 1", description: null, category: "2", assignee: {…}, …} + 4: {id: "15303", name: "Clarify Invoice - Invoice-20302.pdf", description: null, category: null, assignee: {…}, …} + length: 5 + size: 5 + start: 0 + total: 10 We can see that this query resulted in 10 tasks (see `total`), but only 5 were returned as we set `size` to `5`. #### getTotalTasks(requestNode: TaskQueryRequestRepresentationModel): Observable`` + Get total number of tasks matching passed in query definition: ```ts @@ -202,16 +200,15 @@ this.tasklistService.getTotalTasks(taskQuery).subscribe( (response: any) => { This is pretty much the same type of query as the `getTasks` method, except that here we just return how many Task Instances it matched in the `total` property: -``` -data:[] -size: 0 -start: 0 -total: 10 -``` + data:[] + size: 0 + start: 0 + total: 10 When you call this method it always sets the `size` property to `0`. #### findTasksByState(requestNode: TaskQueryRequestRepresentationModel, state?: string): Observable`` + Find and return Task Instances by state `open` or `completed` and query model: ```ts @@ -235,6 +232,7 @@ this.tasklistService.findTasksByState(taskQuery, taskState).subscribe( (taskList console.log('Error: ', error); }); ``` + The number of tasks that are returned is controlled by the `size` property. This is a convenience method on top of the `getTasks` method. It overwrites the `requestNode.state` property @@ -242,6 +240,7 @@ with passed in `state` before making the call to `getTasks`. For an example of the response see the `getTasks` method. #### findAllTaskByState(requestNode: TaskQueryRequestRepresentationModel, state?: string): Observable`` + Find and return all Task Instances by state `open` or `completed` and query model: ```ts @@ -274,6 +273,7 @@ passed in `state` before making any other calls. Before making the `getTasks` ca **Note** that this can return a lot of data if you are not careful. #### findAllTasksWithoutState(requestNode: TaskQueryRequestRepresentationModel): Observable`` + Find and return all Task Instances that matches query model, regardless of state: ```ts @@ -305,6 +305,7 @@ Internally it basically calls `findTasksByState(requestNode, 'open')` and **Note** that this can return a lot of data if you are not careful. #### assignTaskByUserId(taskId: string, userId: number): Observable`` + Assign a Task Instance to a user via the User ID: ```ts @@ -320,6 +321,7 @@ this.tasklistService.assignTaskByUserId(taskId, userId).subscribe( (taskInstance The user ID identifies a User in APS. #### assignTask(taskId: string, requestNode: any): Observable`` + Assign a task to a user via a user object with an `id` property, for example: ```ts @@ -335,7 +337,8 @@ this.tasklistService.assignTask(taskId, user).subscribe( (taskInstance: TaskDeta This method does the same as the `assignTaskByUserId` method, the only difference is that this method can be used when you have an object where the User ID is contained in an `id` property. -#### claimTask(taskId: string): Observable`` +#### claimTask(taskId: string): Observable`` + Claim a pooled task (i.e. candidate task) as current user so it can be worked on and later on completed: ```ts @@ -348,10 +351,11 @@ this.tasklistService.claimTask(taskId).subscribe( (taskInstance: TaskDetailsMode ``` The response will be `null` if the task was claimed successfully. - + The task assignment changes from `candidate` to `assignee`. -#### unclaimTask(taskId: string): Observable`` +#### unclaimTask(taskId: string): Observable`` + Return a claimed task to the pool (i.e. make it a candidate task): ```ts @@ -362,10 +366,11 @@ this.tasklistService.unclaimTask(taskId).subscribe( (taskInstance: TaskDetailsMo console.log('Error: ', error); }); ``` - + The task assignment changes from `assignee` to `candidate`. -#### completeTask(taskId: string) +#### completeTask(taskId: string) + Complete a Task Instance as current user and progress Process Instance: ```ts @@ -377,9 +382,10 @@ This only works if the Task Instance has only one Outcome (i.e. the default `Com If the Task Instance has multiple Outcomes, such as Approve and Reject, then this method does not work, and the Task Instance has to be completed via its associated form. Otherwise you will see an error such as: -*ERROR Error: Uncaught (in promise): Error: {"message":"Task must be completed using it's form","messageKey":"GENERAL.ERROR.BAD-REQUEST"}* +_ERROR Error: Uncaught (in promise): Error: {"message":"Task must be completed using it's form","messageKey":"GENERAL.ERROR.BAD-REQUEST"}_ + +#### updateTask(taskId: any, updated): Observable`` -#### updateTask(taskId: any, updated): Observable`` Update name, description, and due date for a Task Instance: ```ts @@ -395,43 +401,43 @@ this.tasklistService.updateTask(taskId, updateData).subscribe( (updatedTaskDetai console.log('Error: ', error); }); ``` + The response is all info about the updated Task Instance, in this example a stand-alone task was updated so there is no associated process: -``` -adhocTaskCanBeReassigned: false -assignee: undefined -category: null -created: Mon Nov 13 2017 16:34:49 GMT+0000 (GMT) {} -description: "Updated description" -dueDate: Sat Feb 10 2018 11:00:00 GMT+0000 (GMT) {} -duration: NaN -endDate: null -executionId: null -formKey: "5005" -id: "80002" -initiatorCanCompleteTask: false -managerOfCandidateGroup: false -memberOfCandidateGroup: false -memberOfCandidateUsers: false -name: "Updated name" -parentTaskId: null -parentTaskName: null -priority: 50 -processDefinitionCategory: null -processDefinitionDeploymentId: null -processDefinitionDescription: null -processDefinitionId: null -processDefinitionKey: null -processDefinitionName: null -processDefinitionVersion: 0 -processInstanceId: null -processInstanceName: null -processInstanceStartUserId: null -taskDefinitionKey: null -``` + adhocTaskCanBeReassigned: false + assignee: undefined + category: null + created: Mon Nov 13 2017 16:34:49 GMT+0000 (GMT) {} + description: "Updated description" + dueDate: Sat Feb 10 2018 11:00:00 GMT+0000 (GMT) {} + duration: NaN + endDate: null + executionId: null + formKey: "5005" + id: "80002" + initiatorCanCompleteTask: false + managerOfCandidateGroup: false + memberOfCandidateGroup: false + memberOfCandidateUsers: false + name: "Updated name" + parentTaskId: null + parentTaskName: null + priority: 50 + processDefinitionCategory: null + processDefinitionDeploymentId: null + processDefinitionDescription: null + processDefinitionId: null + processDefinitionKey: null + processDefinitionName: null + processDefinitionVersion: 0 + processInstanceId: null + processInstanceName: null + processInstanceStartUserId: null + taskDefinitionKey: null #### createNewTask(task: TaskDetailsModel): Observable`` + Create a new stand-alone Task Instance that is not associated with a Process Instance: ```ts @@ -452,45 +458,44 @@ and that is not assigned to a user (i.e. `assignee: null`). The response looks like this, we can see that an ID was generated for the Task Instance: -``` -Created task details: - adhocTaskCanBeReassigned: false - assignee: null - category: null - created: Mon Nov 13 2017 16:34:49 GMT+0000 (GMT) {} - description: "A new stand-alone task" - dueDate: null - duration: null - endDate: null - executionId: null - formKey: null - id: "80002" - initiatorCanCompleteTask: false - involvedPeople: undefined - managerOfCandidateGroup: false - memberOfCandidateGroup: false - memberOfCandidateUsers: false - name: "Some Task" - parentTaskId: null - parentTaskName: null - priority: 50 - processDefinitionCategory: null - processDefinitionDeploymentId: null - processDefinitionDescription: null - processDefinitionId: null - processDefinitionKey: null - processDefinitionName: null - processDefinitionVersion: 0 - processInstanceId: null - processInstanceName: null - processInstanceStartUserId: null - taskDefinitionKey: null -``` + Created task details: + adhocTaskCanBeReassigned: false + assignee: null + category: null + created: Mon Nov 13 2017 16:34:49 GMT+0000 (GMT) {} + description: "A new stand-alone task" + dueDate: null + duration: null + endDate: null + executionId: null + formKey: null + id: "80002" + initiatorCanCompleteTask: false + involvedPeople: undefined + managerOfCandidateGroup: false + memberOfCandidateGroup: false + memberOfCandidateUsers: false + name: "Some Task" + parentTaskId: null + parentTaskName: null + priority: 50 + processDefinitionCategory: null + processDefinitionDeploymentId: null + processDefinitionDescription: null + processDefinitionId: null + processDefinitionKey: null + processDefinitionName: null + processDefinitionVersion: 0 + processInstanceId: null + processInstanceName: null + processInstanceStartUserId: null + taskDefinitionKey: null See the `attachFormToATask` method for how to attach a form to the User Task. And see the `assignTaskByUserId` method for how to assign a user to the new Task Instance. #### attachFormToATask(taskId: string, formId: number): Observable`` + Attach a form to a User Task: ```ts @@ -510,6 +515,7 @@ in APS when you work with it). See the `getFormList` method for how fetch a list The response will be `null` if form was attached successfully to task. #### getFormList(): Observable`
` + Get a list of the available reusable forms: ```ts @@ -522,16 +528,14 @@ this.tasklistService.getFormList().subscribe( (formList: Form[]) => { A successful response looks like this: -``` -Available forms: + Available forms: -0: - id: 5005 - name: "Name Info" -1: {name: "cm:folder", id: 3012} -2: {name: "Alfresco Node Form", id: 3011} -3: {name: "Employee", id: 3010} -``` + 0: + id: 5005 + name: "Name Info" + 1: {name: "cm:folder", id: 3012} + 2: {name: "Alfresco Node Form", id: 3011} + 3: {name: "Employee", id: 3010} The form id property can be used with the `attachFormToATask` method. @@ -539,6 +543,7 @@ The form id property can be used with the `attachFormToATask` method. Only reusable forms are included. #### addTask(task: TaskDetailsModel): Observable`` + Add a Sub-Task (i.e. checklist task) to a parent Task Instance: ```ts @@ -557,41 +562,40 @@ this.tasklistService.addTask(subTaskDetails).subscribe( (updatedTaskDetails: Tas The response includes the new sub-task id and also parent task name. In this example the parent task was just a stand-alone task, so no process information is included: -``` -adhocTaskCanBeReassigned: false -assignee: null -category: null -created: Tue Nov 14 2017 13:32:41 GMT+0000 (GMT) {} -description: null -dueDate: null -duration: null -endDate: null -executionId: null -formKey: null -id: "80003" -initiatorCanCompleteTask: false -involvedPeople: undefined -managerOfCandidateGroup: false -memberOfCandidateGroup: false -memberOfCandidateUsers: false -name: "Check the invoice amount" -parentTaskId: "80002" -parentTaskName: "Some Task" -priority: 50 -processDefinitionCategory: null -processDefinitionDeploymentId: null -processDefinitionDescription: null -processDefinitionId: null -processDefinitionKey: null -processDefinitionName: null -processDefinitionVersion: 0 -processInstanceId: null -processInstanceName: null -processInstanceStartUserId:null -taskDefinitionKey: null -``` + adhocTaskCanBeReassigned: false + assignee: null + category: null + created: Tue Nov 14 2017 13:32:41 GMT+0000 (GMT) {} + description: null + dueDate: null + duration: null + endDate: null + executionId: null + formKey: null + id: "80003" + initiatorCanCompleteTask: false + involvedPeople: undefined + managerOfCandidateGroup: false + memberOfCandidateGroup: false + memberOfCandidateUsers: false + name: "Check the invoice amount" + parentTaskId: "80002" + parentTaskName: "Some Task" + priority: 50 + processDefinitionCategory: null + processDefinitionDeploymentId: null + processDefinitionDescription: null + processDefinitionId: null + processDefinitionKey: null + processDefinitionName: null + processDefinitionVersion: 0 + processInstanceId: null + processInstanceName: null + processInstanceStartUserId:null + taskDefinitionKey: null #### deleteTask(taskId: string): Observable`` + Delete a Sub-Task (i.e. checklist task): ```ts @@ -606,6 +610,7 @@ this.tasklistService.deleteTask(taskId).subscribe( (taskDetails: TaskDetailsMode **Note**. you can only delete so called checklist tasks with this method. #### fetchTaskAuditJsonById(taskId: string): Observable`` + Fetch Task Audit log as JSON for a Task Instance ID: ```ts @@ -620,18 +625,17 @@ this.tasklistService.fetchTaskAuditJsonById(taskId) The response is JSON object with the Task Instance audit log: -``` -{ - taskId: "15368", - taskName: "Approve by someone in Accounting", - processInstanceId: "15361", - processDefinitionName: "Process With Pooled task", - processDefinitionVersion: 1, - … -} -``` + { + taskId: "15368", + taskName: "Approve by someone in Accounting", + processInstanceId: "15361", + processDefinitionName: "Process With Pooled task", + processDefinitionVersion: 1, + … + } #### fetchTaskAuditPdfById(taskId: string): Observable`` + Fetch Task Audit log as JSON for a Task Instance ID: ```ts @@ -644,8 +648,3 @@ this.tasklistService.fetchTaskAuditPdfById(taskId) ``` The response is PDF with the Task Instance audit log. - - - - - \ No newline at end of file diff --git a/docs/text-highlight.pipe.md b/docs/text-highlight.pipe.md index 9c80cc0978..ca7fb50138 100644 --- a/docs/text-highlight.pipe.md +++ b/docs/text-highlight.pipe.md @@ -17,10 +17,7 @@ be highlighted with added HTML (see the [Highlight Transform service](highlight-transform.service.md) for more information about how this is implemented). The parameter can contain spaces, in which case each separate "word" in the string will be highlighted individually. - - ## See also -- [Highlight directive](highlight.directive.md) -- [Highlight transform service](highlight-transform.service.md) - \ No newline at end of file +- [Highlight directive](highlight.directive.md) +- [Highlight transform service](highlight-transform.service.md) diff --git a/docs/text-mask.component.md b/docs/text-mask.component.md index 564637f894..68b74f4d48 100644 --- a/docs/text-mask.component.md +++ b/docs/text-mask.component.md @@ -11,7 +11,7 @@ Implements text field input masks. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | textMask | any | | Object defining mask and "reversed" status. | ## Details @@ -36,11 +36,11 @@ text mask in ADF would be: The following characters have special meaning within a mask; all other characters are included in the text as they are: -- **"0"**: Denotes a digit -- **"9"**: Denotes a digit that can optionally be left out -- **"A"**: Denotes a alphanumeric character (upper- or lower-case A-Z and digits 0-9) -- **"S"**: Denotes a alphabetic character (upper- or lower-case A-Z) -- **"#"**: Denotes a string of zero or more digits +- **"0"**: Denotes a digit +- **"9"**: Denotes a digit that can optionally be left out +- **"A"**: Denotes a alphanumeric character (upper- or lower-case A-Z and digits 0-9) +- **"S"**: Denotes a alphabetic character (upper- or lower-case A-Z) +- **"#"**: Denotes a string of zero or more digits The mask is passed to the directive in the `mask` field of the parameter object. The `reversed` field indicates that digits in the mask are "filled up" in @@ -60,8 +60,3 @@ However, with reversed (right-to-left) ordering, the textbox would show `2.3` ...and these digits would slide leftward as the user typed more. - - - - - \ No newline at end of file diff --git a/docs/theming.md b/docs/theming.md index 00e106d139..bf404c5ae7 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -22,7 +22,7 @@ Available pre-built themes: If you're using Angular CLI you can include one of the prebuilt theme in your `styles.scss` file: ```css -@import '~ng2-alfresco-core/prebuilt-themes/adf-blue-orange.css'; +@import '~@alfresco/adf-core/prebuilt-themes/adf-blue-orange.css'; ``` Or you can add it directly in your index.html diff --git a/docs/thumbnail.service.md b/docs/thumbnail.service.md index 572aa76a64..d5bbfa4bc8 100644 --- a/docs/thumbnail.service.md +++ b/docs/thumbnail.service.md @@ -20,16 +20,16 @@ a document node or a MIME type. The default mapping between types and icons is shown in the table below: | Document | Icon | Types | -| --- | --- | --- | +| -------- | ---- | ----- | | Compressed archive | ![Archive thumbnail](docassets/images/ft_ic_archive.png) | 'application/x-compressed', 'application/x-zip-compressed', 'application/zip' | | Text | ![Text thumbnail](docassets/images/ft_ic_document.png) | 'text/plain', 'application/json', 'application/x-javascript', 'application/vnd.apple.pages' | | Bitmap/raster image | ![Bitmap thumbnail](docassets/images/ft_ic_raster_image.png) | 'image/png', 'image/jpeg', 'image/gif' | | MP4 video | ![MP4 thumbnail](docassets/images/ft_ic_video.png) | 'video/mp4' | -| SVG vector image| ![SVG thumbnail](docassets/images/ft_ic_vector_image.png) | 'image/svg+xml' | +| SVG vector image | ![SVG thumbnail](docassets/images/ft_ic_vector_image.png) | 'image/svg+xml' | | HTML file | ![HTML thumbnail](docassets/images/ft_ic_website.png) | 'text/html' | | PDF file | ![PDF thumbnail](docassets/images/ft_ic_pdf.png) | 'application/pdf' | | Folder | ![Folder thumbnail](docassets/images/ft_ic_folder.png) | | -| Disabled folder | ![Disabled folder thumbnail](docassets/images/ft_ic_folder_disable.png) | | +| Disabled folder | ![Disabled folder thumbnail](docassets/images/ft_ic_folder_disable.png) | | | Excel spreadsheet | ![Spreadsheet thumbnail](docassets/images/ft_ic_ms_excel.png) | 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' | | PowerPoint slideshow | ![PowerPoint thumbnail](docassets/images/ft_ic_ms_powerpoint.png) | 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.template', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' | | Word document | ![Word thumbnail](docassets/images/ft_ic_ms_word.png) | 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' | @@ -54,14 +54,6 @@ GIF ..... ``` - - - - ## See also -- [Mime type icon pipe](mime-type-icon.pipe.md) - - - - +- [Mime type icon pipe](mime-type-icon.pipe.md) diff --git a/docs/time-ago.pipe.md b/docs/time-ago.pipe.md index 4af4c339d5..17d078232d 100644 --- a/docs/time-ago.pipe.md +++ b/docs/time-ago.pipe.md @@ -15,8 +15,3 @@ Converts a recent past date into a number of days ago. The pipe finds the difference between the input date and the current date. If it is less than seven days then then the date will be formatted as "X days ago". Otherwise, the usual full date format is used. - - - - - \ No newline at end of file diff --git a/docs/toolbar-divider.component.md b/docs/toolbar-divider.component.md index d6ecf6af76..ee9a6e4b64 100644 --- a/docs/toolbar-divider.component.md +++ b/docs/toolbar-divider.component.md @@ -11,4 +11,9 @@ Divides groups of elements in a Toolbar with a visual separator. -``` \ No newline at end of file +``` + +## See Also + +- [Toolbar component](toolbar.component.md) +- [Toolbar Title component](toolbar-title.component.md) diff --git a/docs/toolbar-title.component.md b/docs/toolbar-title.component.md index 5de232d5cd..292701cadf 100644 --- a/docs/toolbar-title.component.md +++ b/docs/toolbar-title.component.md @@ -18,3 +18,8 @@ Supplies custom HTML to be included in a Toolbar component title. ## Details You can use this component to include any HTML or Angular components in the Toolbar title section. + +## See Also + +- [Toolbar component](toolbar.component.md) +- [Toolbar divider component](toolbar-divider.component.md) diff --git a/docs/toolbar.component.md b/docs/toolbar.component.md index a01b67053e..960639546b 100644 --- a/docs/toolbar.component.md +++ b/docs/toolbar.component.md @@ -4,22 +4,6 @@ Simple container for headers, titles, actions and breadcrumbs. ![](docassets/images/adf-toolbar-01.png) - - - - -- [Basic Usage](#basic-usage) - * [Properties](#properties) -- [Details](#details) - * [Custom title](#custom-title) - * [Divider](#divider) - * [Dropdown menu](#dropdown-menu) - * [Custom color](#custom-color) - - - - - ## Basic Usage ```html @@ -36,9 +20,9 @@ Simple container for headers, titles, actions and breadcrumbs. ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| title | string | | Toolbar title | -| color | string | | Toolbar color, can be changed to empty value (default), `primary`, `accent` or `warn`. | +| ---- | ---- | ------- | ----------- | +| title | string | | Toolbar title | +| color | string | | Toolbar color, can be changed to empty value (default), `primary`, `accent` or `warn`. | ## Details @@ -120,4 +104,9 @@ For example: ![](docassets/images/adf-toolbar-06.png) -![](docassets/images/adf-toolbar-07.png) \ No newline at end of file +![](docassets/images/adf-toolbar-07.png) + +## See also + +- [Toolbar Divider component](toolbar-divider.component.md) +- [Toolbar Title component](toolbar-title.component.md) diff --git a/docs/undocStoplist.json b/docs/undocStoplist.json index b0f4f60b20..c978ab9cdc 100644 --- a/docs/undocStoplist.json +++ b/docs/undocStoplist.json @@ -38,5 +38,13 @@ "node-actions", "content-node-selector.service", "analytics", - "process-upload" + "process-upload", + "properties-loader", + "property-descriptors", + "attach-file", + "attach-folder", + "task-upload", + "aspect-whitelist", + "basic-properties", + "content-metadata" ] \ No newline at end of file diff --git a/docs/upload-button.component.md b/docs/upload-button.component.md index be6b899d40..363a877bf5 100644 --- a/docs/upload-button.component.md +++ b/docs/upload-button.component.md @@ -1,5 +1,7 @@ # Upload Button Component +Activates a file upload. + ## Basic Usage ```html diff --git a/docs/upload.directive.md b/docs/upload.directive.md index b1b7b426dd..930853ade1 100644 --- a/docs/upload.directive.md +++ b/docs/upload.directive.md @@ -2,22 +2,6 @@ Allows your components or common HTML elements reacting on File drag and drop in order to upload content. - - - - -- [Basic usage](#basic-usage) -- [Details](#details) - * [Modes](#modes) - + [Click mode](#click-mode) - + [Drop mode](#drop-mode) - * [Events](#events) - * [Styling](#styling) - - - - - ## Basic usage The directive itself does not do any file management process, @@ -55,8 +39,8 @@ Used by attaching to an element or component. Directive supports several modes: -- **drop** mode, where decorated element acts like a drop zone for files (**default** mode) -- **click** mode, where decorated element invokes File Dialog to select files or folders. +- **drop** mode, where decorated element acts like a drop zone for files (**default** mode) +- **click** mode, where decorated element invokes File Dialog to select files or folders. It is also possible combining modes together. @@ -70,9 +54,9 @@ It is also possible combining modes together. For the click mode you can provide additional attributes for the File Dialog: -- **directory**, enables directory selection -- **multiple**, enables multiple file/folder selection -- **accept**, filters the content accepted +- **directory**, enables directory selection +- **multiple**, enables multiple file/folder selection +- **accept**, filters the content accepted ```html
td:last-child { border-right: 1px dashed rgb(68,138,255); } -``` \ No newline at end of file +``` diff --git a/docs/user-info.component.md b/docs/user-info.component.md index 6157a3690d..fe7046a027 100644 --- a/docs/user-info.component.md +++ b/docs/user-info.component.md @@ -1,4 +1,6 @@ -# Alfresco User Info component +# User Info component + +Shows user information. ## Basic usage @@ -9,13 +11,13 @@ ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | ecmBackgroundImage | string | (alfresco image) | Custom path for the background banner image for ECM users | | bpmBackgroundImage | string | (alfresco image) | Custom path for the background banner image for BPM users | -| menuPositionX | string | | Custom choice for opening the menu bottom : `before` or `after` | -| menuPositionY | string | | Custom choice for opening the menu bottom : `above` or `below` | -| namePosition | string | `right` | When the username is showed this define his position relatively the user info button. It can be two values : `right` or `left`| -| showName | boolean | true | Show/Hide the username next the user info button| +| menuPositionX | string | | Custom choice for opening the menu bottom : `before` or `after` | +| menuPositionY | string | | Custom choice for opening the menu bottom : `above` or `below` | +| namePosition | string | `right` | When the username is showed this define his position relatively the user info button. It can be two values : `right` or `left` | +| showName | boolean | true | Show/Hide the username next the user info button | ## Details diff --git a/docs/user-initial.pipe.md b/docs/user-initial.pipe.md index a83eb89d4a..7ca5e08da3 100644 --- a/docs/user-initial.pipe.md +++ b/docs/user-initial.pipe.md @@ -13,14 +13,9 @@ Takes the name fields of a UserProcessModel object and extracts and formats the ## Details The pipe gets the initial characters of the user's first and last names and -concatenates them. The results are returned in an HTML <div> element. +concatenates them. The results are returned in an HTML <div> element. -The first pipe parameter specifies an optional CSS class to add to the <div> +The first pipe parameter specifies an optional CSS class to add to the <div> element (eg, a background color is commonly used to emphasise initials). The second parameter is an optional delimiter to add between the initials. Both parameters default to empty strings. - - - - - \ No newline at end of file diff --git a/docs/user-process.model.md b/docs/user-process.model.md index bfe5872cf3..92cab98473 100644 --- a/docs/user-process.model.md +++ b/docs/user-process.model.md @@ -13,9 +13,6 @@ it is likely to change and so its properties are not listed here. For the latest version of the class, see the [source file](https://github.com/Alfresco/alfresco-ng2-components/blob/development/lib/core/models/user-process.model.ts). - - ## See also -- [People process service](people-process.service.md) - \ No newline at end of file +- [People process service](people-process.service.md) diff --git a/docs/version-list.component.md b/docs/version-list.component.md index 8a9abe2a48..3f3a7d5089 100644 --- a/docs/version-list.component.md +++ b/docs/version-list.component.md @@ -1,7 +1,6 @@ # Version List component -Displays the version history of a node in a -[Version Manager component](version-manager.component.md) +Displays the version history of a node in a [Version Manager component](version-manager.component.md) ### Basic Usage @@ -12,7 +11,7 @@ Displays the version history of a node in a ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | id | string | The node id you want to see the version history of. | ## Details @@ -20,9 +19,6 @@ Displays the version history of a node in a Inside the version manager component, there is the underlying VersionListComponent. The VersionListComponent loads and displays the version history of a node. - - ## See also -- [Version manager component](version-manager.component.md) - \ No newline at end of file +- [Version manager component](version-manager.component.md) diff --git a/docs/version-manager.component.md b/docs/version-manager.component.md index 0891319ccd..97b3f0de9f 100644 --- a/docs/version-manager.component.md +++ b/docs/version-manager.component.md @@ -2,7 +2,7 @@ Displays the version history of a node with the ability to upload a new version. -![#f03c15](https://placehold.it/15/f03c15/000000?text=+) `This component is still in experimental phase, it has several limitations which will be resolved soon.` +![\#f03c15](https://placehold.it/15/f03c15/000000?text=+) `This component is still in experimental phase, it has several limitations which will be resolved soon.` ![Version Manager](docassets/images/version-manager.png) @@ -15,7 +15,7 @@ Displays the version history of a node with the ability to upload a new version. ### Properties | Name | Type | Description | -| --- | --- | --- | +| ---- | ---- | ----------- | | node | [MinimalNodeEntryEntity](https://github.com/Alfresco/alfresco-js-api/blob/master/src/alfresco-core-rest-api/docs/NodeMinimalEntry.md) | The node you want to manage the version history of. | ## Details @@ -24,14 +24,11 @@ Displays the version history of a node with the ability to upload a new version. Each version has a context menu on the right, with the following actions. -| Action | Versions | Description | -| --- | --- | --- | -| Restore | All | Revert the current version to the selected one with creating a new version of it. | +| Action | Versions | Description | +| ------ | -------- | ----------- | +| Restore | All | Revert the current version to the selected one with creating a new version of it. | - - ## See also -- [Version list component](version-list.component.md) -- [Document list component](document-list.component.md) - \ No newline at end of file +- [Version list component](version-list.component.md) +- [Document list component](document-list.component.md) diff --git a/docs/viewer.component.md b/docs/viewer.component.md index 1f22031f41..461fb87414 100644 --- a/docs/viewer.component.md +++ b/docs/viewer.component.md @@ -1,22 +1,27 @@ -# Alfresco Viewer component +# Viewer component + +Displays content from an ACS repository. See it live: [Viewer Quickstart](https://embed.plnkr.co/iTuG1lFIXfsP95l6bDW6/) - +## Contents - +- [Basic usage](#basic-usage) -- [Basic usage](#basic-usage) -- [Properties](#properties) -- [Details](#details) - * [Supported file formats](#supported-file-formats) - * [PDF Conversion](#pdf-conversion) - * [Configuring PDF.js library](#configuring-pdfjs-library) - * [Custom extension handler](#custom-extension-handler) + - [Properties](#properties) + - [Events](#events) - +- [Details](#details) - + - [Integrating with DocumentList component](#integrating-with-documentlist-component) + - [Supported file formats](#supported-file-formats) + - [PDF Conversion](#pdf-conversion) + - [Configuring PDF.js library](#configuring-pdfjs-library) + - [Custom toolbar](#custom-toolbar) + - [Custom sidebar](#custom-sidebar) + - [Custom "Open With" menu](#custom-open-with-menu) + - [Custom "More actions" menu](#custom-more-actions-menu) + - [Extending the Viewer](#extending-the-viewer) ## Basic usage @@ -39,35 +44,40 @@ Using with file url: ``` -## Properties +### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | -| fileNodeId | string | | Node Id of the file to load | -| urlFile | string | | If you want to load an external file that does not come from ECM you can use this Url where to load the file | -| urlBlob | Blob | | If you want to load a Blob File | +| ---- | ---- | ------- | ----------- | +| fileNodeId | string | | Node Id of the file to load | +| sharedLinkId | string | | Shared link id (to display shared file) | +| urlFile | string | | If you want to load an external file that does not come from ECM you can use this Url where to load the file | +| urlFileViewer | string | null | Viewer to use with the `urlFile` address (`pdf`, `image`, `media`, `text`). Used when `urlFile` has no filename and extension. | +| urlBlob | Blob | | If you want to load a Blob File | | overlayMode | boolean | false | If `true` show the Viewer full page over the present content otherwise will fit the parent div | | showViewer | boolean | true | Hide or show the viewer | | showToolbar | boolean | true | Hide or show the toolbar | -| displayName | string | | You can specify the name of the file | +| displayName | string | | You can specify the name of the file | | allowGoBack | boolean | true | Allow `back` navigation | | allowDownload | boolean | true | Toggle download feature | | allowPrint | boolean | false | Toggle printing feature | | allowShare | boolean | false | Toggle sharing feature | -| allowSidebar | boolean |false | Toggle sidebar feature | +| allowSidebar | boolean | false | Toggle sidebar feature | | showSidebar | boolean | false | Toggles sidebar visibility. Requires `allowSidebar` to be set to `true`. | | sidebarPosition | string | right | The position of the sidebar. Can be `left` or `right`. | +| sidebarTemplate | TemplateRef | null | The template intended to be used as a sidebar. The template context contains the loaded node data. | -## Events +### Events | Name | Argument Type | Cancelable | Description | -| --- | --- | --- | --- | +| ---- | ------------- | ---------- | ----------- | | goBack | any | Yes | Raised when user clicks the 'Back' button. | | download | any | Yes | Raised when user clicks the 'Download' button. | | print | any | Yes | Raised when user clicks the 'Print' button. | | share | any | Yes | Raised when user clicks the 'Share' button. | -## Integrating with DocumentList component +## Details + +### Integrating with DocumentList component Below is the most simple integration of Viewer and DocumentList components within your custom component: @@ -103,12 +113,23 @@ export class OverlayViewerComponent { } ``` -## Details +### Custom file parameters + +You can provide custom file parameters, for example a value for the `mimeType` or `displayName` when using URL values that have no file names or extensions: + +```html + + +``` ### Supported file formats | Type | Extension | -| --- | --- | +| ---- | --------- | | Media | wav, Mp3, Mp4, WebM, Ogv | | Images | png, jpg, jpeg, gif, bmp | | Text | pdf, txt | @@ -173,6 +194,13 @@ Everything you put inside the "adf-viewer-toolbar" tags is going to be rendered The Viewer component also supports custom sidebar components and layouts. The `allowSidebar` property should be set to `true` to enable this feature. +Custom sidebar for the viewer can be injected in two different ways: + +- using transclusion +- using template **(only works when using the viewer with fileNodeId)** + +#### Using transclusion + ```html @@ -181,7 +209,16 @@ The `allowSidebar` property should be set to `true` to enable this feature. ``` -Everything you put inside the "adf-viewer-sidebar" tags is going to be rendered instead of the default sidebar. +Everything you put inside the "adf-viewer-sidebar" tags is going to be rendered. + +#### Using template injection (only works when using the viewer with fileNodeId) + +```html + + + + +``` ### Custom "Open With" menu diff --git a/docs/webscript.component.md b/docs/webscript.component.md index 275b2dab59..9cd7d3400a 100644 --- a/docs/webscript.component.md +++ b/docs/webscript.component.md @@ -1,19 +1,6 @@ -# Alfresco Webscript Get component +# Webscript component - - - - -- [Basic usage](#basic-usage) - * [Properties](#properties) -- [Details](#details) - * [Webscript View HTML example](#webscript-view-html-example) - * [Webscript View DATATABLE example](#webscript-view-datatable-example) - * [Webscript View JSON example](#webscript-view-json-example) - - - - +Provides access to Webscript features. ## Basic usage @@ -56,24 +43,23 @@ export class AppComponent { ### Properties | Attribute | Type | Default | Description | -| --- | --- | --- | --- | -| scriptPath | string | | (**mandatory**) path to Web Script (as defined by Web Script) | -| scriptArgs | Object | | arguments to pass to Web Script | -| contextRoot | string | | path where application is deployed | -| scriptPath | string | alfresco | path to Web Script (as defined by Web Script) | -| contentType | string | service | path where Web Script service is mapped default value | +| --------- | ---- | ------- | ----------- | +| scriptPath | string | | (**mandatory**) path to Web Script (as defined by Web Script) | +| scriptArgs | Object | | arguments to pass to Web Script | +| contextRoot | string | | path where application is deployed | +| scriptPath | string | alfresco | path to Web Script (as defined by Web Script) | +| contentType | string | service | path where Web Script service is mapped default value | | contentType | string | TEXT | how to handle the data received from the web script JSON , HTML , DATATABLE or TEXT | | showData | boolean | true | render the webscript data | - ### Events | Name | Description | -| --- | --- | +| ---- | ----------- | | success | You can get the plain data from the webscript through the **success** event parameter and use it as you need in your application | **contentType** {string} -***data*** {string} data containing the plain value you get from the webscipt as an output parameter +**_data_** {string} data containing the plain value you get from the webscipt as an output parameter ## Details @@ -82,7 +68,7 @@ export class AppComponent { This sample demonstrates how to implement a Webscript component that renders the HTML contents that come from a webscript This sample Web Scripts reside in your Alfresco Server. You can access the folder webscript here: -```http://localhost:8080/alfresco/service/sample/folder/Company%20Home``` +`http://localhost:8080/alfresco/service/sample/folder/Company%20Home` ```html -``` +``` ![Custom columns](docassets/images/HTML.png) @@ -100,7 +86,7 @@ This sample Web Scripts reside in your Alfresco Server. You can access the folde This sample demonstrates how to implement a Webscript component that renders the JSON contents that come from a webscript -```http://localhost:8080/alfresco/service/sample/folder/DATATABLE``` +`http://localhost:8080/alfresco/service/sample/folder/DATATABLE` ```html -``` +``` If you want to show the result from a webscript inside a ng2-alfresco-datatable you have to return from the GET of the webscript the datastructure below: subdivide in data and schema @@ -158,7 +144,7 @@ that will render the following table This sample demonstrates how to implement a Webscript component that renders the JSON contents that come from a webscript This sample Web Scripts reside in your Alfresco Server. You can access the folder webscript here: -```http://localhost:8080/alfresco/service/sample/folder/JSON%EXAMPLE``` +`http://localhost:8080/alfresco/service/sample/folder/JSON%EXAMPLE` ```html -``` +``` You can get the plain data from the webscript through the **success** event parameter and use it as you need in your application @@ -178,4 +164,4 @@ You can get the plain data from the webscript through the **success** event para logDataExample(data) { console.log('You webscript data are here' + data); } -``` \ No newline at end of file +``` diff --git a/docs/widget.component.md b/docs/widget.component.md index 023c156c1f..f74e5573de 100644 --- a/docs/widget.component.md +++ b/docs/widget.component.md @@ -20,7 +20,7 @@ export class CustomEditorComponent extends WidgetComponent {} ### Properties | Name | Type | Default | Description | -| --- | --- | --- | --- | +| ---- | ---- | ------- | ----------- | | readOnly | boolean | false | Does the widget show a read-only value? (ie, can't be edited) | | field | [FormFieldModel](form-field.model.md) | | Data to be displayed in the field | @@ -30,9 +30,6 @@ The Widget component is the base class for all standard and custom form widgets. [Form Extensibility and Customisation](extensibility.md) page for full details about implementing custom widgets. - - ## See also -- [Extensibility](extensibility.md) - \ No newline at end of file +- [Extensibility](extensibility.md) diff --git a/lib/config/DocProcessor/docProcessor.js b/lib/config/DocProcessor/docProcessor.js new file mode 100644 index 0000000000..87ce734411 --- /dev/null +++ b/lib/config/DocProcessor/docProcessor.js @@ -0,0 +1,184 @@ +var fs = require("fs"); +var path = require("path"); +var program = require("commander"); + +var remark = require("remark"); +var parse = require("remark-parse"); +var stringify = require("remark-stringify"); + +/* +var managetoc = require("./managetoc"); +var sa = require("./seealso"); +var index = require("./index"); +*/ + +// "Aggregate" data collected over the whole file set. +var aggData = {}; + +var toolsFolderName = "tools"; +var configFileName = "doctool.config.json"; +var defaultFolder = path.resolve("..", "docs"); + + +function initPhase(aggData) { + /* + toolModules.seealso.initPhase(aggData); + toolModules.index.initPhase(aggData); + */ + + toolList.forEach(toolName => { + toolModules[toolName].initPhase(aggData); + }); +} + + +function readPhase(srcFolder, filenames, aggData) { + for (var i = 0; i < filenames.length; i++) { + var pathname = path.resolve(srcFolder, filenames[i]); + + var src = fs.readFileSync(pathname); + var tree = remark().parse(src) + + toolList.forEach(toolName => { + toolModules[toolName].readPhase(tree, pathname, aggData); + }); + + /* + toolModules.seealso.readPhase(tree, pathname, aggData); + toolModules.index.readPhase(tree, pathname, aggData); + */ + } + + //console.log(JSON.stringify(aggData.mdFileData)); +} + + +function aggPhase(aggData) { + toolList.forEach(toolName => { + toolModules[toolName].aggPhase(aggData); + }); + + /* + toolModules.seealso.aggPhase(aggData); + toolModules.index.aggPhase(aggData); + */ +} + + +function updatePhase(srcFolder, destFolder, filenames, aggData) { + for (var i = 0; i < filenames.length; i++) { + var pathname = path.resolve(srcFolder, filenames[i]); + + var src = fs.readFileSync(pathname); + var tree = remark().parse(src) + + var modified = false; + + toolList.forEach(toolName => { + modified |= toolModules[toolName].updatePhase(tree, pathname, aggData); + }); + + if (modified) + fs.writeFileSync(path.resolve(destFolder, filenames[i]), remark().data("settings", {paddedTable: false}).stringify(tree)); + //console.log(JSON.stringify(tree)); + } +} + + +function loadToolModules() { + var mods = {}; + var toolsFolderPath = path.resolve(__dirname, toolsFolderName); + var modFiles = fs.readdirSync(toolsFolderPath); + + for (var i = 0; i < modFiles.length; i++) { + var modPath = path.resolve(toolsFolderPath, modFiles[i]) + + if (path.extname(modPath) === ".js") { + var toolName = path.basename(modPath, ".js"); + mods[toolName] = require(modPath); + } + } + + return mods; +} + + +function loadToolConfig(configFilePath) { + var config = JSON.parse(fs.readFileSync(configFilePath)); + + return config.enabledTools; +} + + +program +.usage("[options] [dest]") +.parse(process.argv); + +if (program.args.length === 0) { + console.log("Error: source argument required"); + return 0; +} + +/* +var sourcePath; + + +if (program.args.length > 0) + sourcePath = path.resolve(program.args[0]); +else + sourcePath = defaultFolder; +*/ +var sourcePath = path.resolve(program.args[0]); +var sourceInfo = fs.statSync(sourcePath); + +var destPath; +var destInfo; + +if (program.args.length >= 2) { + destPath = path.resolve(program.args[1]); + destInfo = fs.statSync(sourcePath); +} else { + destPath = sourcePath; + destInfo = sourceInfo; +} + +if (sourceInfo.isDirectory() && !destInfo.isDirectory()) { + console.log("Error: The argument must be a directory"); + return 0; +} + +var toolModules = loadToolModules(); +var toolList = loadToolConfig(path.resolve(__dirname, configFileName)); + + +var files; + +if (sourceInfo.isDirectory()) { + files = fs.readdirSync(sourcePath); +} else if (sourceInfo.isFile()) { + files = [ path.basename(sourcePath) ]; + sourcePath = path.dirname(sourcePath); + destPath = path.dirname(destPath); +} + +files = files.filter(filename => + (path.extname(filename) === ".md") && + (filename !== "README.md") +); + +//files.forEach(element => console.log(element)); + +console.log("Initialising..."); +initPhase(aggData); + +console.log("Analysing Markdown files..."); +readPhase(sourcePath, files, aggData); + +console.log("Computing aggregate data..."); +aggPhase(aggData); + +console.log("Updating Markdown files..."); +updatePhase(sourcePath, destPath, files, aggData); + + + diff --git a/lib/config/DocProcessor/doctool.config.json b/lib/config/DocProcessor/doctool.config.json new file mode 100644 index 0000000000..6f7ef23478 --- /dev/null +++ b/lib/config/DocProcessor/doctool.config.json @@ -0,0 +1,5 @@ +{ + "enabledTools": [ + "index" + ] +} \ No newline at end of file diff --git a/lib/config/DocProcessor/ngHelpers.js b/lib/config/DocProcessor/ngHelpers.js new file mode 100644 index 0000000000..961234fc2a --- /dev/null +++ b/lib/config/DocProcessor/ngHelpers.js @@ -0,0 +1,19 @@ +module.exports = { + "ngNameToDisplayName": ngNameToDisplayName, + "dekebabifyName": dekebabifyName, + "classTypes": ["component", "directive", "model", "pipe", "service", "widget"] +} + + +function ngNameToDisplayName(ngName) { + var mainSections = ngName.split("."); + mainSections[0] = dekebabifyName(mainSections[0]); + return mainSections.join(" "); +} + + +function dekebabifyName(name) { + var result = name.replace(/-/g, " "); + result = result.substr(0, 1).toUpperCase() + result.substr(1); + return result; +} \ No newline at end of file diff --git a/lib/config/DocProcessor/tools/index.js b/lib/config/DocProcessor/tools/index.js new file mode 100644 index 0000000000..6ce79ec65d --- /dev/null +++ b/lib/config/DocProcessor/tools/index.js @@ -0,0 +1,362 @@ +var fs = require("fs"); +var path = require("path"); + +var remark = require("remark"); +var stringify = require("remark-stringify"); +var zone = require("mdast-zone"); + +var unist = require("../unistHelpers"); +var ngHelpers = require("../ngHelpers"); + + +module.exports = { + "initPhase": initPhase, + "readPhase": readPhase, + "aggPhase": aggPhase, + "updatePhase": updatePhase +} + +var angFilenameRegex = /([a-zA-Z0-9\-]+)\.((component)|(directive)|(model)|(pipe)|(service)|(widget))\.ts/; +var searchFolderOmitRegex = /(config)|(mock)|(i18n)|(assets)|(styles)/; + +var docsFolderPath = path.resolve("..", "docs"); +var undocStoplistFileName = path.resolve(docsFolderPath, "undocStoplist.json"); +var rootFolder = "."; +var indexMdFilePath = path.resolve(docsFolderPath, "README.md"); +var guideSummaryFileName = path.resolve(docsFolderPath, "summary.json"); + +var maxBriefDescLength = 180; + +var adfLibNames = ["core", "content-services", "insights", "process-services"]; + + +function initPhase(aggData) { + aggData.stoplist = makeStoplist(undocStoplistFileName); + aggData.srcData = {}; + aggData.mdFileDesc = []; + + searchLibraryRecursive(aggData.srcData, path.resolve(rootFolder)); + + //console.log(JSON.stringify(aggData.srcData)); +} + + +function readPhase(tree, pathname, aggData) { + var itemName = path.basename(pathname, ".md"); + + // Look for the first paragraph in the file by skipping other items. + // Should usually be a position 1 in the tree. + var s; + var briefDesc; + + for ( + s = 0; + (s < tree.children.length) && !unist.isParagraph(tree.children[s]); + s++ + ); + + if (s < tree.children.length) { + briefDesc = tree.children[s]; + } + + aggData.mdFileDesc[itemName] = briefDesc; +} + +function aggPhase(aggData) { + var sections = prepareIndexSections(aggData); + //console.log(JSON.stringify(sections)); + + var indexFileText = fs.readFileSync(indexMdFilePath, "utf8"); + var indexFileTree = remark().parse(indexFileText); + + for (var l = 0; l < adfLibNames.length; l++) { + + var libName = adfLibNames[l]; + var libSection = sections[libName]; + + var md = makeLibSectionMD(libSection); + + zone(indexFileTree, libName, (startComment, oldSection, endComment) => { + md.unshift(startComment); + md.push(endComment); + return md; + }); + } + + var guideSection = buildGuideSection(guideSummaryFileName); + + zone(indexFileTree, "guide", (startComment, oldSection, endComment) => { + return [ + startComment, guideSection, endComment + ] + }); + + fs.writeFileSync(path.resolve("..", "docs", "README.md"), remark().data("settings", {paddedTable: false}).stringify(indexFileTree)); + //fs.writeFileSync(indexMdFilePath, remark().stringify(indexFileTree)); + //fs.writeFileSync(path.resolve(".", "testJson.md"), JSON.stringify(indexFileTree)); +} + + +function updatePhase(tree, pathname, aggData) { + return false; +} + + +// Create a stoplist of regular expressions. +function makeStoplist(slFilePath) { + var listExpressions = JSON.parse(fs.readFileSync(slFilePath, 'utf8')); + + var result = []; + + for (var i = 0; i < listExpressions.length; i++) { + result.push(new RegExp(listExpressions[i])); + } + + return result; +} + +// Check if an item is covered by the stoplist and reject it if so. +function rejectItemViaStoplist(stoplist, itemName) { + for (var i = 0; i < stoplist.length; i++) { + if (stoplist[i].test(itemName)) { + return true; + } + } + + return false; +} + + +// Search source folders for .ts files to discover all components, directives, etc. +function searchLibraryRecursive(srcData, folderPath) { + var items = fs.readdirSync(folderPath); + + for (var i = 0; i < items.length; i++) { + var itemPath = path.resolve(folderPath, items[i]); + var info = fs.statSync(itemPath); + + if (info.isFile() && (items[i].match(angFilenameRegex))) { + var nameNoSuffix = path.basename(items[i], '.ts'); + + var displayPath = itemPath.replace(/\\/g, '/'); + displayPath = displayPath.substr(displayPath.indexOf("lib") + 4); + + // Type == "component", "directive", etc. + var itemType = nameNoSuffix.split('.')[1]; + + srcData[nameNoSuffix] = { "path": displayPath, "type": itemType }; + } else if (info.isDirectory() && !items[i].match(searchFolderOmitRegex)) { + searchLibraryRecursive(srcData, itemPath); + } + } +} + + +function prepareIndexSections(aggData) { + var srcNames = Object.keys(aggData.srcData); + var sections = initEmptySections(); + + for (var i = 0; i < srcNames.length; i++) { + var itemName = srcNames[i]; + var srcData = aggData.srcData[itemName]; + var libName = srcData.path.substr(0, srcData.path.indexOf("/")); + + var briefDesc = aggData.mdFileDesc[itemName]; + + var displayName = ngHelpers.ngNameToDisplayName(itemName); + + if (briefDesc) { + sections[libName][srcData.type].documented.push({ + "displayName": displayName, + "mdName": itemName + ".md", + "srcPath": srcData.path, + "briefDesc": briefDesc + }); + } else if (!rejectItemViaStoplist(aggData.stoplist, itemName)) { + sections[libName][srcData.type].undocumented.push({ + "displayName": displayName, + "mdName": itemName + ".md", + "srcPath": srcData.path + }); + } + } + + return sections; +} + + +function initEmptySections() { + var result = {}; + + for (var l = 0; l < adfLibNames.length; l++) { + var lib = result[adfLibNames[l]] = {}; + + for (var c = 0; c < ngHelpers.classTypes.length; c++) { + var classType = lib[ngHelpers.classTypes[c]] = {}; + + classType.undocumented = []; + classType.documented = []; + } + } + + return result; +} + + +function buildMDDocumentedSection(docItems) { + var listItems = []; + + for (var i = 0; i < docItems.length; i++) { + listItems.push(makeMDDocumentedListItem(docItems[i])); + } + + return unist.makeListUnordered(listItems); +} + + +function buildMDUndocumentedSection(docItems) { + var listItems = []; + + for (var i = 0; i < docItems.length; i++) { + listItems.push(makeMDUndocumentedListItem(docItems[i])); + } + + return unist.makeListUnordered(listItems); +} + + +function buildMDDocumentedTable(docItems) { + var rows = [ + + ]; + + for (var i = 0; i < docItems.length; i++) { + rows.push(makeMDDocumentedTableRow(docItems[i])); + } + + return rows; + //return unist.makeTable([null, null, null, null], rows); +} + + +function buildMDUndocumentedTable(docItems) { + var rows = [ + + ]; + + for (var i = 0; i < docItems.length; i++) { + rows.push(makeMDUndocumentedTableRow(docItems[i])); + } + + return rows; + //return unist.makeTable([null, null, null, null], rows); +} + + +function makeMDDocumentedListItem(docItem) { + var mdFileLink = unist.makeLink(unist.makeText(docItem.displayName), docItem.mdName); + var srcFileLink = unist.makeLink(unist.makeText("Source"), "../lib/" + docItem.srcPath); + var desc = docItem.briefDesc; + + var para = unist.makeParagraph([ + mdFileLink, unist.makeText(" ("), srcFileLink, unist.makeText(") "), desc + ]); + + return unist.makeListItem(para); +} + + +function makeMDUndocumentedListItem(docItem) { + var itemName = unist.makeText(docItem.displayName); + var srcFileLink = unist.makeLink(unist.makeText("Source"), "../lib/" + docItem.srcPath); + + var para = unist.makeParagraph([ + unist.makeText("* "), itemName, unist.makeText(" ("), srcFileLink, unist.makeText(")") + ]); + + return unist.makeListItem(para); +} + + +function makeMDDocumentedTableRow(docItem) { + var mdFileLink = unist.makeLink(unist.makeText(docItem.displayName), docItem.mdName); + var srcFileLink = unist.makeLink(unist.makeText("Source"), "../lib/" + docItem.srcPath); + var desc = docItem.briefDesc; + + return unist.makeTableRow([ + unist.makeTableCell([mdFileLink]), + unist.makeTableCell([desc]), + unist.makeTableCell([srcFileLink]) + ]); +} + + +function makeMDUndocumentedTableRow(docItem) { + var itemName = unist.makeText(docItem.displayName); + var srcFileLink = unist.makeLink(unist.makeText("Source"), "../lib/" + docItem.srcPath); + + return unist.makeTableRow([ + unist.makeTableCell([unist.makeEmphasis([itemName])]), + unist.makeTableCell([unist.makeEmphasis([unist.makeText("Not currently documented")])]), + unist.makeTableCell([srcFileLink]) + ]); +} + + +function makeLibSectionMD(libSection){ + var md = []; + + var libClassTypes = Object.keys(libSection); + + for (var i = 0; i < libClassTypes.length; i++) { + var classType = libClassTypes[i]; + + var classSection = libSection[classType]; + + if (!classSection) + continue; + + var displayNameNode; + + if ((classSection.documented.length > 0) || (classSection.undocumented.length > 0)) { + displayNameNode = unist.makeText(ngHelpers.dekebabifyName(classType + "s")); + md.push(unist.makeHeading(displayNameNode, 2)); + + var tableRows = [ + unist.makeTableRow([ + unist.makeTableCell([unist.makeText("Name")]), + unist.makeTableCell([unist.makeText("Description")]), + unist.makeTableCell([unist.makeText("Source link")]) + ]) + ]; + + if (classSection.documented.length > 0) { + //md.push(buildMDDocumentedSection(classSection.documented)); + tableRows = tableRows.concat(buildMDDocumentedTable(classSection.documented)); + } + + if (classSection.undocumented.length > 0) { + // md.push(buildMDUndocumentedSection(classSection.undocumented)); + tableRows = tableRows.concat(buildMDUndocumentedTable(classSection.undocumented)); + } + + md.push(unist.makeTable([null, null, null, null], tableRows)); + } + } + + return md; +} + + +function buildGuideSection(guideJsonFilename) { + var summary = JSON.parse(fs.readFileSync(guideJsonFilename, "utf8")); + + var listItems = []; + + for (var i = 0; i < summary.length; i++) { + var link = unist.makeLink(unist.makeText(summary[i].title), summary[i].file); + listItems.push(unist.makeListItem(link)); + } + + return unist.makeListUnordered(listItems); +} \ No newline at end of file diff --git a/lib/config/DocProcessor/tools/seealso.js b/lib/config/DocProcessor/tools/seealso.js new file mode 100644 index 0000000000..47d998370a --- /dev/null +++ b/lib/config/DocProcessor/tools/seealso.js @@ -0,0 +1,175 @@ +var path = require("path"); +var getSection = require("mdast-util-heading-range"); + +var unist = require("../unistHelpers"); + +var seeAlsoHeading = "See Also"; + +module.exports = { + "initPhase": initPhase, + "readPhase": readPhase, + "aggPhase": aggPhase, + "updatePhase": updatePhase +} + + +function initPhase(aggData) { + aggData.saGraph = {}; + aggData.saUpdateGraph = {}; +} + + +function readPhase(tree, pathname, aggData) { + var saHeadingOffset = findSeeAlsoSection(tree); + + var saNode = []; + + if (saHeadingOffset !== -1) { + // Skip over non-list parts. + var s; + + for ( + s = saHeadingOffset; + (s < tree.children.length) && !unist.isListUnordered(tree.children[s]); + s++ + ); + + if ((s < tree.children.length) && unist.isListUnordered(tree.children[s])) { + var list = tree.children[s]; + + for (var i = 0; i < list.children.length; i++) { + var itemLink = getItemLinkInfo(list.children[i]); + + if (itemLink) { + saNode.push(itemLink); + } + } + } + } + + aggData.saGraph[path.basename(pathname, ".md")] = saNode; +} + + +function aggPhase(aggData) { + aggData.saUpdateGraph = tidyGraph(aggData.saGraph); +} + + +function updatePhase(tree, pathname, aggData) { + var currNodeName = path.basename(pathname, ".md"); + var currNodeArcs = aggData.saUpdateGraph[currNodeName]; + + if (currNodeArcs.length > 0) { + var saListItems = []; + + for (var i = 0; i < currNodeArcs.length; i++) { + var linkText = graphKeyToLinkName(currNodeArcs[i]); + var linkTarget = currNodeArcs[i] + ".md"; + var link = unist.makeLink(unist.makeText(linkText), linkTarget); + saListItems.push(unist.makeListItem(link)); + } + + var saHeadingOffset = findSeeAlsoSection(tree); + + if (saHeadingOffset !== -1) { + // Skip over non-list parts. + var s; + + for ( + s = saHeadingOffset; + (s < tree.children.length) && !unist.isListUnordered(tree.children[s]); + s++ + ); + + // Push all elements of the items array as if they were separate elements. + Array.prototype.push.apply(tree.children[s].children, saListItems); + } else { + // This file doesn't currently have a See Also section, so add one at the end. + tree.children.push(unist.makeHeading(unist.makeText(seeAlsoHeading), 2)); + tree.children.push(unist.makeListUnordered(saListItems)); + } + } + + return true; +} + + +function graphKeyToLinkName(key) { + var mainSections = key.split("."); + mainSections[0] = tidyName(mainSections[0]); + return mainSections.join(" "); +} + +// Convert an Angular-style name (eg, "card-view") into one with correct spaces and uppercase (eg, "Card view"). +function tidyName(name) { + var result = name.replace(/-/g, " "); + result = result.substr(0, 1).toUpperCase() + result.substr(1); + return result; +} + + +function makeEmptySAList() { + var result = []; + + +} + + +// Makes link symmetrical between items (ie, if A links to B but not the other way +// around then it adds the missing link). +function tidyGraph(graph) { + var nodeNames = Object.keys(graph); + var result = {}; + + for (var n = 0; n < nodeNames.length; n++) { + result[nodeNames[n]] = []; + } + + for (var n = 0; n < nodeNames.length; n++) { + var currNodeName = nodeNames[n]; + + var currNodeArcs = graph[currNodeName]; + + for (var a = 0; a < currNodeArcs.length; a++) { + var linkedNode = graph[currNodeArcs[a]]; + var resultNode = result[currNodeArcs[a]]; + + if (!linkedNode) { + console.log(`Warning: item '${currNodeArcs[a]}' (in See Also section of '${currNodeName}') has no corresponding file`); + } else if (linkedNode.indexOf(currNodeName) === -1) { + linkedNode.push(currNodeName); + resultNode.push(currNodeName); + } + } + } + + return result; +} + + +function findSeeAlsoSection(tree) { + var i; + + for (i = 0; i < tree.children.length; i++) { + var child = tree.children[i]; + + if (unist.isHeading(child) && (child.children[0].value.toLowerCase() === seeAlsoHeading.toLowerCase())) + return i; + } + + return -1; +} + + +function getItemLinkInfo(listItem) { + var linkTarget = listItem.children[0].children[0].url; + + if (linkTarget.startsWith("http:") || + linkTarget.startsWith("#") || + !linkTarget.endsWith(".md")) + return null; + else + return path.basename(linkTarget, ".md"); +} + diff --git a/lib/config/DocProcessor/tools/toc.js b/lib/config/DocProcessor/tools/toc.js new file mode 100644 index 0000000000..de00d7db76 --- /dev/null +++ b/lib/config/DocProcessor/tools/toc.js @@ -0,0 +1,93 @@ +var remark = require("remark"); +var tocGenerator = require("mdast-util-toc"); +var replaceSection = require("mdast-util-heading-range"); + +var unist = require("../unistHelpers"); + +const contentsHeading = "Contents"; +const minHeadingsForToc = 8; +const maxTocHeadingDepth = 3; + +module.exports = { + "initPhase": initPhase, + "readPhase": readPhase, + "aggPhase": aggPhase, + "updatePhase": updatePhase +} + +function initPhase(aggData) {} +function readPhase(tree, pathname, aggData) {} +function aggPhase(aggData) {} + + +// Find an existing Contents section or add a new empty one if needed. +// Returns true if section is present/needed, false if not needed. +function establishContentsSection(mdTree) { + var firstL2HeadingPos = -1; + var numTocHeadings = 0; + var foundContentsHeading = false; + + for (var i = 0; i < mdTree.children.length; i++) { + var child = mdTree.children[i]; + + // Look through all headings. + if (child.type === "heading") { + + if ((child.depth > 1) && (child.depth <= maxTocHeadingDepth)) { + numTocHeadings++; + } + + if (child.depth === 2) { + // Note where the first L2 heading is. + if (firstL2HeadingPos === -1) { + firstL2HeadingPos = i; + } + + // If it is also a Contents heading then we're done. We don't include the + // Contents heading itself within the ToC, so decrement the count for that. + if ((child.children[0].value === contentsHeading) && !foundContentsHeading) { + foundContentsHeading = true; + numTocHeadings--; + } + } + } + } + + // If we get here then a level 2 Contents heading was not found. + // If there are enough headings for a ToC to be necessary then + // add one in the right place. + if (!foundContentsHeading) { + var newContsHeading = unist.makeHeading(unist.makeText(contentsHeading), 2); + + // If we found another L2 heading then add the Contents in just before it. + if (firstL2HeadingPos != -1) { + mdTree.children.splice(firstL2HeadingPos, 0, newContsHeading); + } else { + // Otherwise, the unlikely situation where a ToC is required but there + // are no L2 headings! Add it as the second element in the document. + mdTree.children.splice(1, 0, newContsHeading); + } + } + + return numTocHeadings; +} + +function updatePhase(tree, pathname, aggData) { +// If we need a contents section then add one or update the existing one. + var numTocHeadings = establishContentsSection(tree); + + if (numTocHeadings >= minHeadingsForToc) { + var newToc = tocGenerator(tree, {heading: contentsHeading, maxDepth: 3}); + + replaceSection(tree, contentsHeading, function(before, oldSection, after) { + return [before, newToc.map, after]; + }); + } else { + // Otherwise, we don't need one, so remove any existing one. + replaceSection(tree, contentsHeading, function(before, oldSection, after) { + return [after]; + }); + } + + return true; +} \ No newline at end of file diff --git a/lib/config/DocProcessor/tools/tscProps.js b/lib/config/DocProcessor/tools/tscProps.js new file mode 100644 index 0000000000..505f7b0314 --- /dev/null +++ b/lib/config/DocProcessor/tools/tscProps.js @@ -0,0 +1,208 @@ +"use strict"; +exports.__esModule = true; +var ts = require("typescript"); +var path = require("path"); +var heading = require("mdast-util-heading-range"); +var remark = require("remark"); +var unist = require("../unistHelpers"); +var typescript_1 = require("typescript"); +function initPhase(aggData) { +} +exports.initPhase = initPhase; +function readPhase(tree, pathname, aggData) { +} +exports.readPhase = readPhase; +function aggPhase(aggData) { +} +exports.aggPhase = aggPhase; +function updatePhase(tree, pathname, aggData) { + var fileNameNoSuffix = path.basename(pathname, ".md"); + if (fileNameNoSuffix.match(/component/)) { + var srcData = aggData.srcData[fileNameNoSuffix]; + if (srcData) { + var srcPath = srcData.path; + var className = fixCompodocFilename(fileNameNoSuffix); + var inputs = []; + var outputs = []; + getPropDocData(path.resolve(".", srcPath), className, inputs, outputs); + var inTable_1 = buildInputsTable(inputs); + var outTable_1 = buildOutputsTable(outputs); + if (inTable_1) { + heading(tree, "Properties", function (before, section, after) { + return [before, inTable_1, after]; + }); + } + if (outTable_1) { + heading(tree, "Events", function (before, section, after) { + return [before, outTable_1, after]; + }); + } + } + } + return true; +} +exports.updatePhase = updatePhase; +function initialCap(str) { + return str[0].toUpperCase() + str.substr(1); +} +function fixCompodocFilename(rawName) { + var name = rawName.replace(/\]|\(|\)/g, ''); + var fileNameSections = name.split('.'); + var compNameSections = fileNameSections[0].split('-'); + var outCompName = ''; + for (var i = 0; i < compNameSections.length; i++) { + outCompName = outCompName + initialCap(compNameSections[i]); + } + var itemTypeIndicator = ''; + if (fileNameSections.length > 1) { + itemTypeIndicator = initialCap(fileNameSections[1]); + } + var finalName = outCompName + itemTypeIndicator; + return finalName; +} +function getPropDocData(srcPath, docClassName, inputs, outputs) { + var prog = ts.createProgram([srcPath], { + target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS + }); + var sourceFiles = prog.getSourceFiles(); + var checker = prog.getTypeChecker(); + for (var i = 0; i < sourceFiles.length; i++) { + if (!sourceFiles[i].isDeclarationFile) + ts.forEachChild(sourceFiles[i], visit); + } + function visit(node) { + if (!isNodeExported(node)) + return; + if (ts.isClassDeclaration(node) && node.name) { + var classDec = node; + var sourceFile = classDec.getSourceFile(); + if (classDec.name.escapedText === docClassName) { + getPropDataFromClassChain(checker, classDec, inputs, outputs); + } + } + } +} +// Get properties/events from main class and all inherited classes. +function getPropDataFromClassChain(checker, classDec, inputs, outputs) { + // Main class + getPropDataFromClass(checker, classDec, inputs, outputs); + // Inherited classes + for (var _i = 0, _a = classDec.heritageClauses; _i < _a.length; _i++) { + var hc = _a[_i]; + var hcType = checker.getTypeFromTypeNode(hc.types[0]); + console.log(checker.getFullyQualifiedName(hcType.symbol)); + for (var _b = 0, _c = hcType.symbol.declarations; _b < _c.length; _b++) { + var dec = _c[_b]; + if (typescript_1.isClassDeclaration(dec)) { + getPropDataFromClassChain(checker, dec, inputs, outputs); + } + } + } +} +function getPropDataFromClass(checker, classDec, inputs, outputs) { + var sourceFile = classDec.getSourceFile(); + for (var i = 0; i < classDec.members.length; i++) { + var member = classDec.members[i]; + if (ts.isPropertyDeclaration(member) || + ts.isGetAccessorDeclaration(member) || + ts.isSetAccessorDeclaration(member)) { + var prop = member; + var mods = ts.getCombinedModifierFlags(prop); + var nonPrivate = (mods & ts.ModifierFlags.Private) === 0; + var memSymbol = checker.getSymbolAtLocation(prop.name); + if (nonPrivate && memSymbol && prop.decorators) { + var name_1 = memSymbol.getName(); + var initializer = ""; + if (prop.initializer) { + initializer = prop.initializer.getText(sourceFile); + } + var doc = ts.displayPartsToString(memSymbol.getDocumentationComment(checker)); + doc = doc.replace(/\r\n/g, " "); + var propType = checker.typeToString(checker.getTypeOfSymbolAtLocation(memSymbol, memSymbol.valueDeclaration)); + var dec = prop.decorators[0].getText(sourceFile); + if (dec.match(/@Input/)) { + inputs.push({ + "name": name_1, + "type": propType, + "init": initializer, + "docText": doc + }); + } + else if (dec.match(/@Output/)) { + outputs.push({ + "name": name_1, + "type": propType, + "docText": doc + }); + } + } + } + } +} +function buildInputsTable(inputs) { + if (inputs.length === 0) { + return null; + } + var rows = [ + unist.makeTableRow([ + unist.makeTableCell([unist.makeText("Name")]), + unist.makeTableCell([unist.makeText("Type")]), + // unist.makeTableCell([unist.makeText("Default value")]), + unist.makeTableCell([unist.makeText("Description")]) + ]) + ]; + for (var i = 0; i < inputs.length; i++) { + var pName = inputs[i].name; + var pType = inputs[i].type; + var pDefault = inputs[i].init || ""; + var pDesc = inputs[i].docText || ""; + if (pDesc) { + //pDesc = pDesc.trim().replace(/[\n\r]+/, " "); + pDesc = pDesc.replace(/[\n\r]+/, " "); + } + var descCellContent = remark().parse(pDesc).children; + if (pDefault) { + descCellContent.push(unist.makeHTML("
")); + descCellContent.push(unist.makeText(" Default value: ")); + descCellContent.push(unist.makeInlineCode(pDefault)); + } + var cells = [ + unist.makeTableCell([unist.makeText(pName)]), + unist.makeTableCell([unist.makeInlineCode(pType)]), + //unist.makeTableCell([unist.makeText(pDefault)]), + unist.makeTableCell(descCellContent) + ]; + rows.push(unist.makeTableRow(cells)); + } + return unist.makeTable([null, null, null, null], rows); +} +function buildOutputsTable(outputs) { + if (outputs.length === 0) { + return null; + } + var rows = [ + unist.makeTableRow([ + unist.makeTableCell([unist.makeText("Name")]), + unist.makeTableCell([unist.makeText("Type")]), + unist.makeTableCell([unist.makeText("Description")]) + ]) + ]; + for (var i = 0; i < outputs.length; i++) { + var eName = outputs[i].name; + var eType = outputs[i].type; + var eDesc = outputs[i].docText || ""; + if (eDesc) { + eDesc = eDesc.trim().replace(/[\n\r]+/, ' '); + } + var cells = [ + unist.makeTableCell([unist.makeText(eName)]), + unist.makeTableCell([unist.makeInlineCode(eType)]), + unist.makeTableCell(remark().parse(eDesc).children) + ]; + rows.push(unist.makeTableRow(cells)); + } + return unist.makeTable([null, null, null], rows); +} +function isNodeExported(node) { + return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile); +} diff --git a/lib/config/DocProcessor/tools/tscProps.ts b/lib/config/DocProcessor/tools/tscProps.ts new file mode 100644 index 0000000000..eda11a78d3 --- /dev/null +++ b/lib/config/DocProcessor/tools/tscProps.ts @@ -0,0 +1,278 @@ +import * as ts from "typescript"; +import * as fs from "fs"; +import * as path from "path"; +import * as program from "commander"; + +import * as heading from "mdast-util-heading-range"; +import * as remark from "remark"; + +import * as unist from "../unistHelpers"; +import { JsxEmit, isClassDeclaration } from "typescript"; + +export function initPhase(aggData) { +} + +export function readPhase(tree, pathname, aggData) { +} + +export function aggPhase(aggData) { +} + +export function updatePhase(tree, pathname, aggData) { + let fileNameNoSuffix = path.basename(pathname, ".md"); + + if (fileNameNoSuffix.match(/component/)) { + let srcData = aggData.srcData[fileNameNoSuffix]; + + if (srcData) { + let srcPath = srcData.path; + let className = fixCompodocFilename(fileNameNoSuffix); + + let inputs = []; + let outputs = []; + getPropDocData(path.resolve(".", srcPath), className, inputs, outputs); + + let inTable = buildInputsTable(inputs); + let outTable = buildOutputsTable(outputs); + + if (inTable) { + heading(tree, "Properties", (before, section, after) => { + return [before, inTable, after]; + }); + } + + if (outTable) { + heading(tree, "Events", (before, section, after) => { + return [before, outTable, after]; + }); + } + } + } + + return true; +} + + +function initialCap(str: string) { + return str[0].toUpperCase() + str.substr(1); +} + + +function fixCompodocFilename(rawName: string) { + var name = rawName.replace(/\]|\(|\)/g, ''); + + var fileNameSections = name.split('.'); + var compNameSections = fileNameSections[0].split('-'); + + var outCompName = ''; + + for (var i = 0; i < compNameSections.length; i++) { + outCompName = outCompName + initialCap(compNameSections[i]); + } + + var itemTypeIndicator = ''; + + if (fileNameSections.length > 1) { + itemTypeIndicator = initialCap(fileNameSections[1]); + } + + var finalName = outCompName + itemTypeIndicator; + + return finalName; +} + + +function getPropDocData(srcPath: string, docClassName: string, inputs: any[], outputs: any[]) { + let prog = ts.createProgram([srcPath], { + target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS + }); + + let sourceFiles = prog.getSourceFiles(); + let checker = prog.getTypeChecker(); + + for (var i = 0; i < sourceFiles.length; i++) { + if (!sourceFiles[i].isDeclarationFile) + ts.forEachChild(sourceFiles[i], visit); + } + + function visit(node: ts.Node) { + if (!isNodeExported(node)) + return; + + if (ts.isClassDeclaration(node) && node.name) { + let classDec: ts.ClassDeclaration = node; + let sourceFile = classDec.getSourceFile(); + + if (classDec.name.escapedText === docClassName) { + getPropDataFromClassChain(checker, classDec, inputs, outputs); + } + } + } +} + + +// Get properties/events from main class and all inherited classes. +function getPropDataFromClassChain( + checker: ts.TypeChecker, + classDec: ts.ClassDeclaration, + inputs: any[], + outputs: any[] +){ + // Main class + getPropDataFromClass(checker, classDec, inputs, outputs); + + // Inherited classes + for(const hc of classDec.heritageClauses) { + let hcType = checker.getTypeFromTypeNode(hc.types[0]); + + console.log(checker.getFullyQualifiedName(hcType.symbol)); + + for (const dec of hcType.symbol.declarations) { + if (isClassDeclaration(dec)) { + getPropDataFromClassChain(checker, dec, inputs, outputs); + } + } + } + +} + + +function getPropDataFromClass( + checker: ts.TypeChecker, + classDec: ts.ClassDeclaration, + inputs: any[], + outputs: any[] +){ + let sourceFile = classDec.getSourceFile(); + + for (var i = 0; i < classDec.members.length; i++) { + let member = classDec.members[i]; + + if (ts.isPropertyDeclaration(member) || + ts.isGetAccessorDeclaration(member) || + ts.isSetAccessorDeclaration(member)) { + let prop: ts.PropertyDeclaration = member; + + let mods = ts.getCombinedModifierFlags(prop); + let nonPrivate = (mods & ts.ModifierFlags.Private) === 0; + let memSymbol = checker.getSymbolAtLocation(prop.name); + + if (nonPrivate && memSymbol && prop.decorators) { + let name = memSymbol.getName(); + let initializer = ""; + + if (prop.initializer) { + initializer = prop.initializer.getText(sourceFile); + } + + let doc = ts.displayPartsToString(memSymbol.getDocumentationComment(checker)); + doc = doc.replace(/\r\n/g, " "); + + let propType = checker.typeToString(checker.getTypeOfSymbolAtLocation(memSymbol, memSymbol.valueDeclaration!)); + + let dec = prop.decorators[0].getText(sourceFile); + + if (dec.match(/@Input/)) { + inputs.push({ + "name": name, + "type": propType, + "init": initializer, + "docText": doc + }); + } else if (dec.match(/@Output/)) { + outputs.push({ + "name": name, + "type": propType, + "docText": doc + }); + } + } + } + } +} + + +function buildInputsTable(inputs: any[]) { + if (inputs.length === 0) { + return null; + } + + var rows = [ + unist.makeTableRow([ + unist.makeTableCell([unist.makeText("Name")]), + unist.makeTableCell([unist.makeText("Type")]), + // unist.makeTableCell([unist.makeText("Default value")]), + unist.makeTableCell([unist.makeText("Description")]) + ]) + ]; + + for (var i = 0; i < inputs.length; i++) { + var pName = inputs[i].name; + var pType = inputs[i].type; + var pDefault = inputs[i].init || ""; + var pDesc = inputs[i].docText || ""; + + if (pDesc) { + //pDesc = pDesc.trim().replace(/[\n\r]+/, " "); + pDesc = pDesc.replace(/[\n\r]+/, " "); + } + + var descCellContent = remark().parse(pDesc).children; + + if (pDefault) { + descCellContent.push(unist.makeHTML("
")); + descCellContent.push(unist.makeText(" Default value: ")); + descCellContent.push(unist.makeInlineCode(pDefault)); + } + + var cells = [ + unist.makeTableCell([unist.makeText(pName)]), + unist.makeTableCell([unist.makeInlineCode(pType)]), + //unist.makeTableCell([unist.makeText(pDefault)]), + unist.makeTableCell(descCellContent) + ]; + + rows.push(unist.makeTableRow(cells)); + } + + return unist.makeTable([null, null, null, null], rows); +} + + +function buildOutputsTable(outputs: any[]) { + if (outputs.length === 0) { + return null; + } + + var rows = [ + unist.makeTableRow([ + unist.makeTableCell([unist.makeText("Name")]), + unist.makeTableCell([unist.makeText("Type")]), + unist.makeTableCell([unist.makeText("Description")]) + ]) + ]; + + for (var i = 0; i < outputs.length; i++){ + var eName = outputs[i].name; + var eType = outputs[i].type; + var eDesc = outputs[i].docText || ""; + + if (eDesc) { + eDesc = eDesc.trim().replace(/[\n\r]+/, ' '); + } + + var cells = [ + unist.makeTableCell([unist.makeText(eName)]), + unist.makeTableCell([unist.makeInlineCode(eType)]), + unist.makeTableCell(remark().parse(eDesc).children) + ]; + + rows.push(unist.makeTableRow(cells)); + } + + return unist.makeTable([null, null, null], rows); +} + +function isNodeExported(node: ts.Node): boolean { + return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile); +} \ No newline at end of file diff --git a/lib/config/DocProcessor/unistHelpers.js b/lib/config/DocProcessor/unistHelpers.js new file mode 100644 index 0000000000..597ae764d4 --- /dev/null +++ b/lib/config/DocProcessor/unistHelpers.js @@ -0,0 +1,122 @@ +module.exports = { + + makeRoot: function (children) { + return { + "type": "root", + "children": children + }; + }, + + makeText: function (textVal) { + return { + "type": "text", + "value": textVal + }; + }, + + makeEmphasis: function (content) { + return { + "type": "emphasis", + "children": content + }; + }, + + makeStrong: function (content) { + return { + "type": "strong", + "children": content + }; + }, + + makeHeading: function (caption, depth) { + return { + "type": "heading", + "depth": depth, + "children": [caption] + }; + }, + + makeLink: function (caption, url) { + return { + "type": "link", + "url": url, + "children": [ caption ] + }; + }, + + makeListItem: function (itemValue) { + return { + "type": "listItem", + "loose": false, + "children": [ itemValue ] + }; + }, + + makeListUnordered: function (itemsArray) { + return { + "type": "list", + "ordered": false, + "children": itemsArray, + "loose": false + }; + }, + + makeParagraph: function (itemsArray) { + return { + "type": "paragraph", + "children": itemsArray + } + }, + + makeTable: function (colAlignArray, rowArray) { + return { + "type": "table", + "align": colAlignArray, + "children": rowArray + }; + }, + + makeTableRow: function (cellArray) { + return { + "type": "tableRow", + "children": cellArray + }; + }, + + makeTableCell: function (content) { + return { + "type": "tableCell", + "children": content + }; + }, + + makeInlineCode: function (codeText) { + return { + "type": "inlineCode", + "value": codeText + } + }, + + makeHTML: function (htmlText) { + return { + "type": "html", + "value": htmlText + }; + }, + + isHeading: function (node) { + return node.type === "heading"; + }, + + isListUnordered: function (node) { + return (node.type === "list") && !node.ordered; + }, + + isParagraph: function (node) { + return node.type === "paragraph"; + }, + + isText: function (node) { + return node.type === "text"; + } +} \ No newline at end of file diff --git a/lib/config/karma.conf-all.js b/lib/config/karma.conf-all.js index e3fbc071e3..540f03f4fe 100644 --- a/lib/config/karma.conf-all.js +++ b/lib/config/karma.conf-all.js @@ -1,4 +1,5 @@ const webpackCoverage = require('./webpack.coverage'); +const webpackTest= require('./webpack.test'); module.exports = function (config) { var _config = { @@ -46,7 +47,7 @@ module.exports = function (config) { {pattern: './config/app.config.json', included: false, served: true, watched: false} ], - webpack: webpackCoverage(config), + webpack: (config.mode === 'coverage') ? webpackCoverage(config) : webpackTest(config), webpackMiddleware: { noInfo: true, @@ -69,11 +70,9 @@ module.exports = function (config) { autoWatch: false, - captureTimeout: 4800000, - browserDisconnectTimeout: 4800000, - browserDisconnectTolerance: 10, - browserNoActivityTimeout: 3000000, - + browserDisconnectTimeout: 200000, + browserNoActivityTimeout: 2400000, + captureTimeout: 1200000, browsers: ['Chrome'], // Karma plugins loaded @@ -114,10 +113,5 @@ module.exports = function (config) { } }; - if (process.env.TRAVIS) { - config.browsers = ['ChromeHeadless']; - config.reporters = ['dots', 'coverage']; - } - config.set(_config); }; diff --git a/lib/config/webpack.coverage.js b/lib/config/webpack.coverage.js index fa4d1dbc42..4632ecdad5 100644 --- a/lib/config/webpack.coverage.js +++ b/lib/config/webpack.coverage.js @@ -1,15 +1,25 @@ const webpack = require('webpack'); const webpackMerge = require('webpack-merge'); -const testConfig = require('./webpack.test.js'); +const commonConfig = require('./webpack.common.js'); const helpers = require('./helpers'); module.exports = function (config) { - return webpackMerge(testConfig, { + return webpackMerge(commonConfig, { devtool: 'inline-source-map', module: { rules: [ + { + test: /\.(txt|pdf)$/, + loader: 'file-loader', + query: { + name: '[path][name].[ext]', + outputPath: (url)=> { + return url.replace('src', 'dist'); + } + } + }, { enforce: 'post', test: /^(?!(.*spec|index|.*mock|.*model|.*event)).*\.ts?$/, diff --git a/lib/config/webpack.test.js b/lib/config/webpack.test.js index 5a54422886..364d4b5bf9 100644 --- a/lib/config/webpack.test.js +++ b/lib/config/webpack.test.js @@ -1,22 +1,24 @@ const webpackMerge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); -module.exports = webpackMerge(commonConfig, { +module.exports = function (config) { + return webpackMerge(commonConfig, { - devtool: 'inline-source-map', + devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.(txt|pdf)$/, - loader: 'file-loader', - query: { - name: '[path][name].[ext]', - outputPath: (url)=> { + module: { + rules: [ + { + test: /\.(txt|pdf)$/, + loader: 'file-loader', + query: { + name: '[path][name].[ext]', + outputPath: (url)=> { return url.replace('src', 'dist'); + } } } - } - ] - } -}); + ] + } + }); +}; diff --git a/lib/content-services/breadcrumb/breadcrumb.component.ts b/lib/content-services/breadcrumb/breadcrumb.component.ts index ae63523d48..9679c7690e 100644 --- a/lib/content-services/breadcrumb/breadcrumb.component.ts +++ b/lib/content-services/breadcrumb/breadcrumb.component.ts @@ -30,15 +30,26 @@ import { DocumentListComponent } from '../document-list'; }) export class BreadcrumbComponent implements OnChanges { + /** Active node, builds UI based on folderNode.path.elements collection. */ @Input() folderNode: MinimalNodeEntryEntity = null; + /** (optional) Name of the root element of the breadcrumb. You can use + * this property to rename "Company Home" to "Personal Files" for + * example. You can use an i18n resource key for the property value. + */ @Input() root: string = null; + /** (optional) The id of the root element. You can use this property + * to set a custom element the breadcrumb should start with. + */ @Input() rootId: string = null; + /** (optional) Document List component to operate with. The list will + * update when the breadcrumb is clicked. + */ @Input() target: DocumentListComponent; @@ -48,6 +59,7 @@ export class BreadcrumbComponent implements OnChanges { return !!this.root; } + /** Emitted when the user clicks on a breadcrumb. */ @Output() navigate: EventEmitter = new EventEmitter(); diff --git a/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.html b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.html new file mode 100644 index 0000000000..f0a23fc436 --- /dev/null +++ b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.html @@ -0,0 +1,28 @@ + + + + + + + diff --git a/lib/content-services/content-metadata/content-metadata-card.component.scss b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.scss similarity index 68% rename from lib/content-services/content-metadata/content-metadata-card.component.scss rename to lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.scss index 9426378da6..81a5a73fd4 100644 --- a/lib/content-services/content-metadata/content-metadata-card.component.scss +++ b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.scss @@ -3,9 +3,18 @@ $background: map-get($theme, background); $foreground: map-get($theme, foreground); - .adf-viewer-default-sidebar { + .adf-content-metadata-card { - &-card-footer.mat-card-footer { + .mat-card { + padding: 0; + + .mat-card-content { + margin-bottom: 0; + } + } + + &-footer.mat-card-footer { + margin: 0; padding: 8px 12px; border-top: 1px solid mat-color($foreground, text, 0.07); diff --git a/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts new file mode 100644 index 0000000000..04522dacc0 --- /dev/null +++ b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.spec.ts @@ -0,0 +1,172 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/*tslint:disable: ban*/ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { ContentMetadataCardComponent } from './content-metadata-card.component'; +import { ContentMetadataComponent } from '../content-metadata/content-metadata.component'; +import { MatExpansionModule, MatCardModule, MatButtonModule, MatIconModule } from '@angular/material'; +import { ContentMetadataService } from '../../services/content-metadata.service'; +import { BasicPropertiesService } from '../../services/basic-properties.service'; +import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service'; +import { PropertyDescriptorsService } from '../../services/property-descriptors.service'; +import { AspectWhiteListService } from '../../services/aspect-whitelist.service'; +import { AlfrescoApiService } from '@alfresco/adf-core'; + +describe('ContentMetadataCardComponent', () => { + + let component: ContentMetadataCardComponent, + fixture: ComponentFixture, + node: MinimalNodeEntryEntity, + preset = 'custom-preset'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatExpansionModule, + MatCardModule, + MatButtonModule, + MatIconModule + ], + declarations: [ + ContentMetadataCardComponent, + ContentMetadataComponent + ], + providers: [ + ContentMetadataService, + BasicPropertiesService, + PropertyDescriptorLoaderService, + PropertyDescriptorsService, + AspectWhiteListService, + AlfrescoApiService + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentMetadataCardComponent); + component = fixture.componentInstance; + node = { + aspectNames: [], + content: {}, + properties: {}, + createdByUser: {}, + modifiedByUser: {} + }; + + component.node = node; + component.preset = preset; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + it('should have displayEmpty input param as false by default', () => { + expect(component.displayEmpty).toBe(false); + }); + + it('should pass through the node to the underlying component', () => { + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance; + + expect(contentMetadataComponent.node).toBe(node); + }); + + it('should pass through the preset to the underlying component', () => { + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance; + + expect(contentMetadataComponent.preset).toBe(preset); + }); + + it('should pass through the preset to the underlying component', () => { + component.displayEmpty = true; + fixture.detectChanges(); + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance; + + expect(contentMetadataComponent.displayEmpty).toBe(true); + }); + + it('should pass through the editable to the underlying component', () => { + component.editable = true; + fixture.detectChanges(); + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance; + + expect(contentMetadataComponent.editable).toBe(true); + }); + + it('should pass through the expanded to the underlying component', () => { + component.expanded = true; + fixture.detectChanges(); + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance; + + expect(contentMetadataComponent.expanded).toBe(true); + }); + + it('should not show anything if node is not present', () => { + component.node = undefined; + fixture.detectChanges(); + + const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)); + + expect(contentMetadataComponent).toBeNull(); + }); + + it('should toggle editable by clicking on the button', () => { + component.editable = true; + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-edit"]')); + button.triggerEventHandler('click', {}); + fixture.detectChanges(); + + expect(component.editable).toBe(false); + }); + + it('should toggle expanded by clicking on the button', () => { + component.expanded = true; + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand"]')); + button.triggerEventHandler('click', {}); + fixture.detectChanges(); + + expect(component.expanded).toBe(false); + }); + + it('should have the proper text on button while collapsed', () => { + component.expanded = false; + fixture.detectChanges(); + + const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand-label"]')); + + expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION'); + }); + + it('should have the proper text on button while collapsed', () => { + component.expanded = true; + fixture.detectChanges(); + + const buttonLabel = fixture.debugElement.query(By.css('[data-automation-id="mata-data-card-toggle-expand-label"]')); + + expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION'); + }); +}); diff --git a/lib/content-services/content-metadata/content-metadata-card.component.ts b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.ts similarity index 85% rename from lib/content-services/content-metadata/content-metadata-card.component.ts rename to lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.ts index 5021a57c01..fc1a6b5f23 100644 --- a/lib/content-services/content-metadata/content-metadata-card.component.ts +++ b/lib/content-services/content-metadata/components/content-metadata-card/content-metadata-card.component.ts @@ -18,19 +18,23 @@ import { Component, Input, ViewEncapsulation } from '@angular/core'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -const PROPERTY_COUNTER_WHILE_COLLAPSED = 5; - @Component({ selector: 'adf-content-metadata-card', templateUrl: './content-metadata-card.component.html', styleUrls: ['./content-metadata-card.component.scss'], encapsulation: ViewEncapsulation.None, - host: { 'class': 'adf-viewer-default-sidebar' } + host: { 'class': 'adf-content-metadata-card' } }) export class ContentMetadataCardComponent { @Input() node: MinimalNodeEntryEntity; + @Input() + displayEmpty: boolean = false; + + @Input() + preset: string; + editable: boolean = false; expanded: boolean = false; @@ -41,8 +45,4 @@ export class ContentMetadataCardComponent { toggleExpanded(): void { this.expanded = !this.expanded; } - - get maxPropertiesToShow(): number { - return this.expanded ? Infinity : PROPERTY_COUNTER_WHILE_COLLAPSED; - } } diff --git a/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.html b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.html new file mode 100644 index 0000000000..81cab09477 --- /dev/null +++ b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.html @@ -0,0 +1,43 @@ + \ No newline at end of file diff --git a/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.scss b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.scss new file mode 100644 index 0000000000..ef15fefd80 --- /dev/null +++ b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.scss @@ -0,0 +1,9 @@ +@mixin adf-content-metadata-theme($theme) { + .adf { + &-metadata-properties { + .mat-expansion-panel:not([class*=mat-elevation-z]) { + box-shadow: none; + } + } + } +} diff --git a/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.spec.ts b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.spec.ts new file mode 100644 index 0000000000..63d63447fd --- /dev/null +++ b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.spec.ts @@ -0,0 +1,258 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/*tslint:disable: ban*/ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SimpleChange } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { ContentMetadataComponent } from './content-metadata.component'; +import { MatExpansionModule, MatButtonModule, MatIconModule } from '@angular/material'; +import { ContentMetadataService } from '../../services/content-metadata.service'; +import { BasicPropertiesService } from '../../services/basic-properties.service'; +import { PropertyDescriptorLoaderService } from '../../services/properties-loader.service'; +import { PropertyDescriptorsService } from '../../services/property-descriptors.service'; +import { AspectWhiteListService } from '../../services/aspect-whitelist.service'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core'; +import { ErrorObservable } from 'rxjs/observable/ErrorObservable'; +import { Observable } from 'rxjs/Observable'; + +describe('ContentMetadataComponent', () => { + + let component: ContentMetadataComponent, + fixture: ComponentFixture, + node: MinimalNodeEntryEntity, + preset = 'custom-preset'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatExpansionModule, + MatButtonModule, + MatIconModule + ], + declarations: [ + ContentMetadataComponent + ], + providers: [ + ContentMetadataService, + BasicPropertiesService, + PropertyDescriptorLoaderService, + PropertyDescriptorsService, + AspectWhiteListService, + AlfrescoApiService, + NodesApiService, + { provide: LogService, useValue: { error: jasmine.createSpy('error') } } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentMetadataComponent); + component = fixture.componentInstance; + node = { + id: 'node-id', + aspectNames: [], + content: {}, + properties: {}, + createdByUser: {}, + modifiedByUser: {} + }; + + component.node = node; + component.preset = preset; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('Default input param values', () => { + + it('should have editable input param as false by default', () => { + expect(component.editable).toBe(false); + }); + + it('should have displayEmpty input param as false by default', () => { + expect(component.displayEmpty).toBe(false); + }); + + it('should have expanded input param as false by default', () => { + expect(component.expanded).toBe(false); + }); + }); + + describe('Saving', () => { + + it('should save the node on itemUpdate', () => { + const property = { key: 'property-key', value: 'original-value' }, + updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService), + nodesApiService: NodesApiService = TestBed.get(NodesApiService); + spyOn(nodesApiService, 'updateNode'); + + updateService.update(property, 'updated-value'); + + expect(nodesApiService.updateNode).toHaveBeenCalledWith('node-id', { + 'property-key': 'updated-value' + }); + }); + + it('should update the node on successful save', async(() => { + const property = { key: 'property-key', value: 'original-value' }, + updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService), + nodesApiService: NodesApiService = TestBed.get(NodesApiService), + expectedNode = Object.assign({}, node, { name: 'some-modified-value' }); + + spyOn(nodesApiService, 'updateNode').and.callFake(() => { + return Observable.of(expectedNode); + }); + + updateService.update(property, 'updated-value'); + + fixture.whenStable().then(() => { + expect(component.node).toBe(expectedNode); + }); + })); + + it('should throw error on unsuccessful save', () => { + const property = { key: 'property-key', value: 'original-value' }, + updateService: CardViewUpdateService = fixture.debugElement.injector.get(CardViewUpdateService), + nodesApiService: NodesApiService = TestBed.get(NodesApiService), + logService: LogService = TestBed.get(LogService); + + spyOn(nodesApiService, 'updateNode').and.callFake(() => { + return ErrorObservable.create(new Error('My bad')); + }); + + updateService.update(property, 'updated-value'); + + expect(logService.error).toHaveBeenCalledWith(new Error('My bad')); + }); + }); + + describe('Properties loading', () => { + + let expectedNode, + contentMetadataService: ContentMetadataService; + + beforeEach(() => { + expectedNode = Object.assign({}, node, { name: 'some-modified-value' }); + contentMetadataService = TestBed.get(ContentMetadataService); + fixture.detectChanges(); + }); + + it('should load the basic properties on node change', () => { + spyOn(contentMetadataService, 'getBasicProperties'); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + expect(contentMetadataService.getBasicProperties).toHaveBeenCalledWith(expectedNode); + }); + + it('should pass through the loaded basic properties to the card view', async(() => { + const expectedProperties = []; + component.expanded = false; + fixture.detectChanges(); + spyOn(contentMetadataService, 'getBasicProperties').and.callFake(() => { + return Observable.of(expectedProperties); + }); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + component.basicProperties$.subscribe(() => { + fixture.detectChanges(); + const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance; + expect(basicPropertiesComponent.properties).toBe(expectedProperties); + }); + })); + + it('should pass through the displayEmpty to the card view of basic properties', async(() => { + component.displayEmpty = false; + fixture.detectChanges(); + spyOn(contentMetadataService, 'getBasicProperties').and.returnValue(Observable.of([])); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + component.basicProperties$.subscribe(() => { + fixture.detectChanges(); + const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance; + expect(basicPropertiesComponent.displayEmpty).toBe(false); + }); + })); + + it('should load the aspect properties on node change', () => { + spyOn(contentMetadataService, 'getAspectProperties'); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + expect(contentMetadataService.getAspectProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset'); + }); + + it('should pass through the loaded aspect properties to the card view', async(() => { + const expectedProperties = []; + component.expanded = true; + fixture.detectChanges(); + spyOn(contentMetadataService, 'getAspectProperties').and.callFake(() => { + return Observable.of([{ properties: expectedProperties }]); + }); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + component.basicProperties$.subscribe(() => { + fixture.detectChanges(); + const firstAspectPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance; + expect(firstAspectPropertiesComponent.properties).toBe(expectedProperties); + }); + })); + + it('should pass through the displayEmpty to the card view of aspect properties', async(() => { + component.expanded = true; + component.displayEmpty = false; + fixture.detectChanges(); + spyOn(contentMetadataService, 'getAspectProperties').and.returnValue(Observable.of([{ properties: [] }])); + + component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) }); + + component.basicProperties$.subscribe(() => { + fixture.detectChanges(); + const basicPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-properties-aspect adf-card-view')).componentInstance; + expect(basicPropertiesComponent.displayEmpty).toBe(false); + }); + })); + + it('should be performed again if property updating occured, since the originally passed node has changed, so the previously calculated properties', () => { + const property = { key: 'property-key', value: 'original-value' }, + updateService = fixture.debugElement.injector.get(CardViewUpdateService), + nodesApiService = TestBed.get(NodesApiService); + + spyOn(nodesApiService, 'updateNode').and.callFake(() => Observable.of(node)); + spyOn(contentMetadataService, 'getBasicProperties'); + component.ngOnChanges({ node: new SimpleChange(null, node, true) }); + updateService.update(property, 'updated-value'); + + component.ngOnChanges({ expanded: new SimpleChange(false, true, false) }); + component.ngOnChanges({ expanded: new SimpleChange(true, false, false) }); + component.ngOnChanges({ expanded: new SimpleChange(false, true, false) }); + + expect(contentMetadataService.getBasicProperties).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/lib/content-services/content-metadata/content-metadata.component.ts b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.ts similarity index 52% rename from lib/content-services/content-metadata/content-metadata.component.ts rename to lib/content-services/content-metadata/components/content-metadata/content-metadata.component.ts index a39afedc53..72f6739b3e 100644 --- a/lib/content-services/content-metadata/content-metadata.component.ts +++ b/lib/content-services/content-metadata/components/content-metadata/content-metadata.component.ts @@ -15,21 +15,20 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, Input, OnChanges, OnInit, SimpleChanges, SimpleChange, ViewEncapsulation } from '@angular/core'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; -import { CardViewItem, CardViewUpdateService, FileSizePipe, NodesApiService } from '@alfresco/adf-core'; -import { ContentMetadataService } from './content-metadata.service'; +import { CardViewItem, CardViewUpdateService, NodesApiService, LogService } from '@alfresco/adf-core'; +import { ContentMetadataService } from '../../services/content-metadata.service'; +import { CardViewAspect } from '../../interfaces/content-metadata.interfaces'; @Component({ selector: 'adf-content-metadata', templateUrl: './content-metadata.component.html', styleUrls: ['./content-metadata.component.scss'], host: { 'class': 'adf-content-metadata' }, - changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [ CardViewUpdateService ], - viewProviders: [ ContentMetadataService, FileSizePipe ] + providers: [ CardViewUpdateService ] }) export class ContentMetadataComponent implements OnChanges, OnInit { @@ -40,43 +39,47 @@ export class ContentMetadataComponent implements OnChanges, OnInit { editable: boolean = false; @Input() - maxPropertiesToShow: number = Infinity; + displayEmpty: boolean = false; - properties: CardViewItem[] = []; + @Input() + expanded: boolean = false; + + @Input() + preset: string; + + nodeHasBeenUpdated: boolean = false; + basicProperties$: Observable; + aspects$: Observable; constructor(private contentMetadataService: ContentMetadataService, private cardViewUpdateService: CardViewUpdateService, - private nodesApi: NodesApiService) {} + private nodesApi: NodesApiService, + private logService: LogService) {} - ngOnInit(): void { + ngOnInit() { this.cardViewUpdateService.itemUpdated$ .switchMap(this.saveNode.bind(this)) .subscribe( - node => this.node = node, - error => this.handleError(error) + (node) => { + this.nodeHasBeenUpdated = true; + this.node = node; + }, + error => this.logService.error(error) ); } - ngOnChanges(): void { - this.recalculateProperties(); + ngOnChanges(changes: SimpleChanges) { + const nodeChange: SimpleChange = changes['node']; + if (nodeChange || this.nodeHasBeenUpdated) { + const node = nodeChange && nodeChange.currentValue || this.node; + this.nodeHasBeenUpdated = false; + + this.basicProperties$ = this.contentMetadataService.getBasicProperties(node); + this.aspects$ = this.contentMetadataService.getAspectProperties(node, this.preset); + } } private saveNode({ changed: nodeBody }): Observable { return this.nodesApi.updateNode(this.node.id, nodeBody); } - - private handleError(error): void { - /*tslint:disable-next-line*/ - console.log(error); - } - - private recalculateProperties(): void { - let basicProperties = this.contentMetadataService.getBasicProperties(this.node); - - if (this.maxPropertiesToShow) { - basicProperties = basicProperties.slice(0, this.maxPropertiesToShow); - } - - this.properties = [...basicProperties]; - } } diff --git a/lib/content-services/content-metadata/content-metadata-card.component.html b/lib/content-services/content-metadata/content-metadata-card.component.html deleted file mode 100644 index 5dea07f211..0000000000 --- a/lib/content-services/content-metadata/content-metadata-card.component.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -
- - -
- -
-
diff --git a/lib/content-services/content-metadata/content-metadata.component.html b/lib/content-services/content-metadata/content-metadata.component.html deleted file mode 100644 index b9eae6c36d..0000000000 --- a/lib/content-services/content-metadata/content-metadata.component.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/lib/content-services/content-metadata/content-metadata.component.scss b/lib/content-services/content-metadata/content-metadata.component.scss deleted file mode 100644 index 25222c4a69..0000000000 --- a/lib/content-services/content-metadata/content-metadata.component.scss +++ /dev/null @@ -1,29 +0,0 @@ -@mixin adf-content-metadata-theme($theme) { - $primary: map-get($theme, primary); - $background: map-get($theme, background); - $foreground: map-get($theme, foreground); - - .adf { - &-metadata-properties { - - .adf-property { - display: block; - margin-bottom: 20px; - - .adf-property-label { - display: block; - padding: 0; - font-size: 12px; - color: mat-color($foreground, text, 0.4); - } - - .adf-property-value { - display: block; - padding: 0; - font-size: 14px; - color: mat-color($foreground, text, 0.87); - } - } - } - } -} diff --git a/lib/content-services/content-metadata/content-metadata.module.scss b/lib/content-services/content-metadata/content-metadata.module.scss new file mode 100644 index 0000000000..4c043f0d19 --- /dev/null +++ b/lib/content-services/content-metadata/content-metadata.module.scss @@ -0,0 +1,7 @@ +@import './components/content-metadata/content-metadata.component'; +@import './components/content-metadata-card/content-metadata-card.component'; + +@mixin adf-content-metadata-module-theme($theme) { + @include adf-content-metadata-theme($theme); + @include adf-content-metadata-card-theme($theme); +} diff --git a/lib/content-services/content-metadata/content-metadata.module.ts b/lib/content-services/content-metadata/content-metadata.module.ts index 5d4e76b863..55f84715bf 100644 --- a/lib/content-services/content-metadata/content-metadata.module.ts +++ b/lib/content-services/content-metadata/content-metadata.module.ts @@ -20,10 +20,14 @@ import { FlexLayoutModule } from '@angular/flex-layout'; import { NgModule } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { MaterialModule } from '../material.module'; -import { CardViewModule } from '@alfresco/adf-core'; - -import { ContentMetadataComponent } from './content-metadata.component'; -import { ContentMetadataCardComponent } from './content-metadata-card.component'; +import { CardViewModule , FileSizePipe } from '@alfresco/adf-core'; +import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component'; +import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component'; +import { PropertyDescriptorsService } from './services/property-descriptors.service'; +import { PropertyDescriptorLoaderService } from './services/properties-loader.service'; +import { AspectWhiteListService } from './services/aspect-whitelist.service'; +import { BasicPropertiesService } from './services/basic-properties.service'; +import { ContentMetadataService } from './services/content-metadata.service'; @NgModule({ imports: [ @@ -40,6 +44,14 @@ import { ContentMetadataCardComponent } from './content-metadata-card.component' declarations: [ ContentMetadataComponent, ContentMetadataCardComponent + ], + providers: [ + ContentMetadataService, + PropertyDescriptorsService, + PropertyDescriptorLoaderService, + AspectWhiteListService, + BasicPropertiesService, + FileSizePipe ] }) export class ContentMetadataModule {} diff --git a/lib/content-services/content-metadata/interfaces/aspect-property.interface.ts b/lib/content-services/content-metadata/interfaces/aspect-property.interface.ts new file mode 100644 index 0000000000..c88f3d2ba5 --- /dev/null +++ b/lib/content-services/content-metadata/interfaces/aspect-property.interface.ts @@ -0,0 +1,26 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export interface AspectProperty { + name: string; + title: string; + description?: string; + dataType: string; + defaultValue?: any; + mandatory: boolean; + multiValued: boolean; +} diff --git a/lib/content-services/content-metadata/interfaces/aspect.interface.ts b/lib/content-services/content-metadata/interfaces/aspect.interface.ts new file mode 100644 index 0000000000..5c02e3c886 --- /dev/null +++ b/lib/content-services/content-metadata/interfaces/aspect.interface.ts @@ -0,0 +1,25 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { AspectProperty } from "./aspect-property.interface"; + +export interface Aspect { + name: string; + title: string; + description: string; + properties: AspectProperty[] +} diff --git a/lib/content-services/content-metadata/interfaces/card-view-aspect.interface.ts b/lib/content-services/content-metadata/interfaces/card-view-aspect.interface.ts new file mode 100644 index 0000000000..af07068985 --- /dev/null +++ b/lib/content-services/content-metadata/interfaces/card-view-aspect.interface.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItem} from '@alfresco/adf-core'; + +export interface CardViewAspect { + name: string; + properties: CardViewItem[] +} diff --git a/lib/content-services/content-metadata/interfaces/content-metadata.interfaces.ts b/lib/content-services/content-metadata/interfaces/content-metadata.interfaces.ts new file mode 100644 index 0000000000..d9ec4f5016 --- /dev/null +++ b/lib/content-services/content-metadata/interfaces/content-metadata.interfaces.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './aspect-property.interface'; +export * from './aspect.interface'; +export * from './card-view-aspect.interface'; \ No newline at end of file diff --git a/lib/content-services/content-metadata/public-api.ts b/lib/content-services/content-metadata/public-api.ts index dd66fdddb0..ecb6235824 100644 --- a/lib/content-services/content-metadata/public-api.ts +++ b/lib/content-services/content-metadata/public-api.ts @@ -15,5 +15,5 @@ * limitations under the License. */ -export * from './content-metadata.component'; -export * from './content-metadata.service'; +export * from './components/content-metadata-card/content-metadata-card.component'; +export { ContentMetadataModule } from './content-metadata.module'; diff --git a/lib/content-services/content-metadata/services/aspect-whitelist.service.ts b/lib/content-services/content-metadata/services/aspect-whitelist.service.ts new file mode 100644 index 0000000000..5c23391c9e --- /dev/null +++ b/lib/content-services/content-metadata/services/aspect-whitelist.service.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Injectable } from '@angular/core'; +import { AppConfigService, LogService } from '@alfresco/adf-core'; + +@Injectable() +export class AspectWhiteListService { + + static readonly DEFAULT_PRESET = '*'; + static readonly DEFAULT_PRESET_NAME = 'default'; + + preset: object | string = AspectWhiteListService.DEFAULT_PRESET; + + constructor(private appConfigService: AppConfigService, + private logService: LogService) {} + + public choosePreset(presetName: string) { + try { + const preset = this.appConfigService.config['content-metadata'].presets[presetName]; + + if (preset) { + this.preset = preset; + } else if (presetName !== AspectWhiteListService.DEFAULT_PRESET_NAME) { + this.logService.error(`No content-metadata preset for: ${presetName}`); + } + } catch (e) { + this.preset = AspectWhiteListService.DEFAULT_PRESET; + } + } + + public isAspectAllowed(aspectName) { + if (this.isEveryAspectAllowed) { + return true; + } + + const aspectNames = Object.keys(this.preset); + return aspectNames.indexOf(aspectName) !== -1; + } + + public isPropertyAllowed(aspectName, propertyName) { + if (this.isEveryAspectAllowed || this.isEveryPropertyAllowedFor(aspectName)) { + return true; + } + + if (this.preset[aspectName]) { + return this.preset[aspectName].indexOf(propertyName) !== -1; + } + + return false; + } + + private get isEveryAspectAllowed(): boolean { + return typeof this.preset === 'string' && this.preset === AspectWhiteListService.DEFAULT_PRESET; + } + + private isEveryPropertyAllowedFor(aspectName): boolean { + const whitedListedProperties = this.preset[aspectName]; + return typeof whitedListedProperties === 'string' && whitedListedProperties === AspectWhiteListService.DEFAULT_PRESET; + } +} diff --git a/lib/content-services/content-metadata/content-metadata.service.ts b/lib/content-services/content-metadata/services/basic-properties.service.ts similarity index 98% rename from lib/content-services/content-metadata/content-metadata.service.ts rename to lib/content-services/content-metadata/services/basic-properties.service.ts index 06d05cd540..a57afd8739 100644 --- a/lib/content-services/content-metadata/content-metadata.service.ts +++ b/lib/content-services/content-metadata/services/basic-properties.service.ts @@ -20,7 +20,7 @@ import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core'; @Injectable() -export class ContentMetadataService { +export class BasicPropertiesService { constructor(private fileSizePipe: FileSizePipe) {} diff --git a/lib/content-services/content-metadata/services/content-metadata.service.spec.ts b/lib/content-services/content-metadata/services/content-metadata.service.spec.ts new file mode 100644 index 0000000000..a0ec319c14 --- /dev/null +++ b/lib/content-services/content-metadata/services/content-metadata.service.spec.ts @@ -0,0 +1,325 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { ContentMetadataService } from './content-metadata.service'; +import { PropertyDescriptorsService } from './property-descriptors.service'; +import { BasicPropertiesService } from './basic-properties.service'; +import { AspectWhiteListService } from './aspect-whitelist.service'; +import { PropertyDescriptorLoaderService } from './properties-loader.service'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { + CardViewTextItemModel, + CardViewDateItemModel, + CardViewIntItemModel, + CardViewFloatItemModel, + LogService, + CardViewBoolItemModel, + CardViewDatetimeItemModel +} from '@alfresco/adf-core'; + +describe('ContentMetadataService', () => { + + let service: ContentMetadataService, + descriptorsService: PropertyDescriptorsService, + aspects: Aspect[], + aspect: Aspect, + aspectProperty: AspectProperty, + node: MinimalNodeEntryEntity; + + const dummyPreset = 'default'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers: [ + ContentMetadataService, + BasicPropertiesService, + AspectWhiteListService, + PropertyDescriptorLoaderService, + AlfrescoApiService, + { provide: LogService, useValue: { error: () => {} }}, + PropertyDescriptorsService + ] + }).compileComponents(); + })); + + beforeEach(() => { + service = TestBed.get(ContentMetadataService); + descriptorsService = TestBed.get(PropertyDescriptorsService); + + node = { properties: {} }; + aspectProperty = { + name: 'FAS:PLAGUE', + title: 'The Faro Plague', + dataType: '', + defaultValue: '', + mandatory: false, + multiValued: false + }; + aspect = { + name: 'FAS:FAS', + title: 'Faro Automated Solutions', + description: 'Faro Automated Solutions is an old Earth corporation that manufactured robots in the mid-21st century.', + properties: [aspectProperty] + }; + aspects = []; + spyOn(descriptorsService, 'getAspects').and.returnValue(Observable.of(aspects)); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + describe('General transformation', () => { + + it('should translate more properties in an aspect properly', () => { + aspect.properties = [{ + name: 'FAS:PLAGUE', + title: 'title', + dataType: 'd:text', + defaultValue: 'defaultValue', + mandatory: false, + multiValued: false + }, + { + name: 'FAS:ALOY', + title: 'title', + dataType: 'd:text', + defaultValue: 'defaultValue', + mandatory: false, + multiValued: false + }]; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': 'The Chariot Line' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + expect(cardViewAspect[0].properties.length).toBe(2); + expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First property should be instance of CardViewTextItemModel'); + expect(cardViewAspect[0].properties[1] instanceof CardViewTextItemModel).toBeTruthy('Second property should be instance of CardViewTextItemModel'); + }); + }); + + it('should translate every property in every aspect properly', () => { + aspects.push( + Object.assign({}, aspect, { + properties: [{ + name: 'FAS:PLAGUE', + title: 'title', + dataType: 'd:text', + defaultValue: 'defaultvalue', + mandatory: false, + multiValued: false + }] + }), + Object.assign({}, aspect, { + properties: [{ + name: 'FAS:ALOY', + title: 'title', + dataType: 'd:text', + defaultValue: 'defaultvalue', + mandatory: false, + multiValued: false + }] + }) + ); + + node.properties = { 'FAS:PLAGUE': 'The Chariot Line' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + expect(cardViewAspect.length).toBe(2); + expect(cardViewAspect[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First aspect\'s property should be instance of CardViewTextItemModel'); + expect(cardViewAspect[1].properties[0] instanceof CardViewTextItemModel).toBeTruthy('Second aspect\'s property should be instance of CardViewTextItemModel'); + }); + }); + + it('should log an error if unrecognised type is found', () => { + const logService = TestBed.get(LogService); + spyOn(logService, 'error').and.stub(); + + aspectProperty.name = 'FAS:PLAGUE'; + aspectProperty.title = 'The Faro Plague'; + aspectProperty.dataType = 'daemonic:scorcher'; + aspectProperty.defaultValue = 'Daemonic beast'; + aspects.push(aspect); + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher'); + }); + }); + + it('should fall back to singleline property type if unrecognised type is found', () => { + aspectProperty.name = 'FAS:PLAGUE'; + aspectProperty.title = 'The Faro Plague'; + aspectProperty.dataType = 'daemonic:scorcher'; + aspectProperty.defaultValue = 'Daemonic beast'; + aspects.push(aspect); + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewTextItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel'); + }); + }); + }); + + describe('Different types\'s attributes', () => { + + ContentMetadataService.RECOGNISED_ECM_TYPES.forEach((dataType) => { + it(`should translate properly the basic attributes of a property for ${dataType}`, () => { + aspectProperty.name = 'prefix:name'; + aspectProperty.title = 'title'; + aspectProperty.defaultValue = 'default value'; + aspectProperty.dataType = dataType; + aspects.push(aspect); + + node.properties = { 'prefix:name': null }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property = cardViewAspect[0].properties[0]; + expect(property.label).toBe(aspectProperty.title); + expect(property.key).toBe('properties.prefix:name'); + expect(property.default).toBe(aspectProperty.defaultValue); + expect(property.editable).toBeTruthy('Property should be editable'); + }); + }); + }); + + it('should translate properly the multiline and value attributes for d:text', () => { + aspectProperty.dataType = 'd:text'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': 'The Chariot Line' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewTextItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel'); + expect(property.value).toBe('The Chariot Line'); + expect(property.multiline).toBeFalsy('Property should be singleline'); + }); + }); + + it('should translate properly the multiline and value attributes for d:mltext', () => { + aspectProperty.dataType = 'd:mltext'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': 'The Chariot Line' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewTextItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel'); + expect(property.value).toBe('The Chariot Line'); + expect(property.multiline).toBeTruthy('Property should be multiline'); + }); + }); + + it('should translate properly the value attribute for d:date', () => { + const expectedValue = new Date().toISOString(); + aspectProperty.dataType = 'd:date'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': expectedValue }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewDateItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewDateItemModel).toBeTruthy('Property should be instance of CardViewDateItemModel'); + expect(property.value).toBe(expectedValue); + }); + }); + + it('should translate properly the value attribute for d:date', () => { + const expectedValue = new Date().toISOString(); + aspectProperty.dataType = 'd:datetime'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': expectedValue }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewDatetimeItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewDatetimeItemModel).toBeTruthy('Property should be instance of CardViewDatetimeItemModel'); + expect(property.value).toBe(expectedValue); + }); + }); + + it('should translate properly the value attribute for d:int', () => { + aspectProperty.dataType = 'd:int'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': '1024' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewIntItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel'); + expect(property.value).toBe(1024); + }); + }); + + it('should translate properly the value attribute for d:long', () => { + aspectProperty.dataType = 'd:long'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': '1024' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewIntItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel'); + expect(property.value).toBe(1024); + }); + }); + + it('should translate properly the value attribute for d:float', () => { + aspectProperty.dataType = 'd:float'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': '1024.24' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewFloatItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel'); + expect(property.value).toBe(1024.24); + }); + }); + + it('should translate properly the value attribute for d:double', () => { + aspectProperty.dataType = 'd:double'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': '1024.24' }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewFloatItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel'); + expect(property.value).toBe(1024.24); + }); + }); + + it('should translate properly the value attribute for d:boolean', () => { + aspectProperty.dataType = 'd:boolean'; + aspects.push(aspect); + + node.properties = { 'FAS:PLAGUE': true }; + + service.getAspectProperties(node, dummyPreset).subscribe((cardViewAspect) => { + const property: CardViewBoolItemModel = cardViewAspect[0].properties[0]; + expect(property instanceof CardViewBoolItemModel).toBeTruthy('Property should be instance of CardViewBoolItemModel'); + expect(property.value).toBe(true); + }); + }); + }); +}); diff --git a/lib/content-services/content-metadata/services/content-metadata.service.ts b/lib/content-services/content-metadata/services/content-metadata.service.ts new file mode 100644 index 0000000000..924aa9ced7 --- /dev/null +++ b/lib/content-services/content-metadata/services/content-metadata.service.ts @@ -0,0 +1,139 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Injectable } from '@angular/core'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { PropertyDescriptorsService } from './property-descriptors.service'; +import { BasicPropertiesService } from './basic-properties.service'; +import { + CardViewItemProperties, + CardViewItem, + CardViewTextItemModel, + CardViewBoolItemModel, + CardViewDateItemModel, + CardViewDatetimeItemModel, + CardViewIntItemModel, + CardViewFloatItemModel, + LogService +} from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { AspectProperty, CardViewAspect, Aspect } from '../interfaces/content-metadata.interfaces'; + +const D_TEXT = 'd:text'; +const D_MLTEXT = 'd:mltext'; +const D_DATE = 'd:date'; +const D_DATETIME = 'd:datetime'; +const D_INT = 'd:int'; +const D_LONG = 'd:long'; +const D_FLOAT = 'd:float'; +const D_DOUBLE = 'd:double'; +const D_BOOLEAN = 'd:boolean'; + +@Injectable() +export class ContentMetadataService { + + static readonly RECOGNISED_ECM_TYPES = [ D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT, D_LONG , D_FLOAT, D_DOUBLE, D_BOOLEAN ]; + + constructor(private basicPropertiesService: BasicPropertiesService, + private propertyDescriptorsService: PropertyDescriptorsService, + private logService: LogService) {} + + getBasicProperties(node: MinimalNodeEntryEntity): Observable { + return Observable.of(this.basicPropertiesService.getBasicProperties(node)); + } + + getAspectProperties(node: MinimalNodeEntryEntity, preset: string): Observable { + return this.propertyDescriptorsService.getAspects(node, preset) + .map(aspects => this.translateAspects(aspects, node.properties)); + } + + private translateAspects(aspects: Aspect[], nodeProperties): CardViewAspect[] { + return aspects.map(aspect => { + const translatedAspect: any = Object.assign({}, aspect); + translatedAspect.properties = this.translateProperties(aspect.properties, nodeProperties); + return translatedAspect; + }); + } + + private translateProperties(aspectProperties: AspectProperty[], nodeProperties: any): CardViewItem[] { + return aspectProperties.map(aspectProperty => { + return this.translateProperty(aspectProperty, nodeProperties[aspectProperty.name]); + }); + } + + private translateProperty(aspectProperty: AspectProperty, nodeProperty: any): CardViewItem { + this.checkECMTypeValidity(aspectProperty.dataType); + + let propertyDefinition: CardViewItemProperties = { + label: aspectProperty.title, + value: nodeProperty, + key: this.getAspectPropertyKey(aspectProperty.name), + default: aspectProperty.defaultValue, + editable: true + }; + let property; + + switch (aspectProperty.dataType) { + + case D_MLTEXT: + property = new CardViewTextItemModel(Object.assign(propertyDefinition, { + multiline: true + })); + break; + + case D_INT: + case D_LONG: + property = new CardViewIntItemModel(propertyDefinition); + break; + + case D_FLOAT: + case D_DOUBLE: + property = new CardViewFloatItemModel(propertyDefinition); + break; + + case D_DATE: + property = new CardViewDateItemModel(propertyDefinition); + break; + + case D_DATETIME: + property = new CardViewDatetimeItemModel(propertyDefinition); + break; + + case D_BOOLEAN: + property = new CardViewBoolItemModel(propertyDefinition); + break; + + case D_TEXT: + default: + property = new CardViewTextItemModel(Object.assign(propertyDefinition, { + multiline: false + })); + } + + return property; + } + + private checkECMTypeValidity(ecmPropertyType) { + if (ContentMetadataService.RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) { + this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`); + } + } + + private getAspectPropertyKey(propertyName) { + return `properties.${propertyName}`; + } +} diff --git a/lib/content-services/content-metadata/services/properties-loader.service.spec.ts b/lib/content-services/content-metadata/services/properties-loader.service.spec.ts new file mode 100644 index 0000000000..5211866aa3 --- /dev/null +++ b/lib/content-services/content-metadata/services/properties-loader.service.spec.ts @@ -0,0 +1,95 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { PropertyDescriptorLoaderService } from './properties-loader.service'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { ClassesApi } from 'alfresco-js-api'; + +describe('PropertyDescriptorLoaderService', () => { + + let aspectProperties: PropertyDescriptorLoaderService, + classesApi: ClassesApi; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers: [ + PropertyDescriptorLoaderService, + AlfrescoApiService + ] + }).compileComponents(); + })); + + beforeEach(() => { + aspectProperties = TestBed.get(PropertyDescriptorLoaderService); + const alfrescoApiService = TestBed.get(AlfrescoApiService); + classesApi = alfrescoApiService.classesApi; + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + it('should load the aspects passed by paramter', () => { + spyOn(classesApi, 'getClass'); + + aspectProperties.load(['exif:exif', 'cm:content', 'custom:custom']) + .subscribe(() => {}); + + expect(classesApi.getClass).toHaveBeenCalledTimes(3); + expect(classesApi.getClass).toHaveBeenCalledWith('exif_exif'); + expect(classesApi.getClass).toHaveBeenCalledWith('cm_content'); + expect(classesApi.getClass).toHaveBeenCalledWith('custom_custom'); + }); + + it('should merge the forked values', (done) => { + + const exifResponse = { + name: 'exif:exif', + id: 1, + properties: { + 'exif:1': { id: 'exif:1:id', name: 'exif:1' }, + 'exif:2': { id: 'exif:2:id', name: 'exif:2' } + } + }; + + const contentResponse = { + name: 'cm:content', + id: 2, + properties: { + 'cm:content': { id: 'cm:content:id', name: 'cm:content' } + } + }; + + const apiResponses = [ exifResponse, contentResponse ]; + let counter = 0; + + spyOn(classesApi, 'getClass').and.callFake(() => { + return Observable.of(apiResponses[counter++]); + }); + + aspectProperties.load(['exif:exif', 'cm:content']) + .subscribe({ + next: (data) => { + expect(data[0]).toBe(exifResponse); + expect(data[1]).toBe(contentResponse); + }, + complete: done + }); + }); +}); diff --git a/lib/content-services/content-metadata/services/properties-loader.service.ts b/lib/content-services/content-metadata/services/properties-loader.service.ts new file mode 100644 index 0000000000..406a1b63a0 --- /dev/null +++ b/lib/content-services/content-metadata/services/properties-loader.service.ts @@ -0,0 +1,36 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Injectable } from '@angular/core'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { forkJoin } from 'rxjs/observable/forkJoin'; +import { Observable } from 'rxjs/Observable'; +import { defer } from 'rxjs/observable/defer'; + +@Injectable() +export class PropertyDescriptorLoaderService { + + constructor(private alfrescoApiService: AlfrescoApiService) {} + + load(aspects: string[]): Observable { + const aspectFetchStreams = aspects + .map(aspectName => aspectName.replace(':', '_')) + .map(aspectName => defer( () => this.alfrescoApiService.classesApi.getClass(aspectName)) ); + + return forkJoin(aspectFetchStreams); + } +} diff --git a/lib/content-services/content-metadata/services/property-descriptors.service.spec.ts b/lib/content-services/content-metadata/services/property-descriptors.service.spec.ts new file mode 100644 index 0000000000..9eb76f98f2 --- /dev/null +++ b/lib/content-services/content-metadata/services/property-descriptors.service.spec.ts @@ -0,0 +1,209 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { PropertyDescriptorsService } from './property-descriptors.service'; +import { PropertyDescriptorLoaderService } from './properties-loader.service'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { AlfrescoApiService } from '@alfresco/adf-core'; +import { AspectWhiteListService } from './aspect-whitelist.service'; +import { AppConfigService, LogService } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { AspectProperty } from '../interfaces/content-metadata.interfaces'; + +describe('PropertyDescriptorsService', () => { + + let contentMetadataService: PropertyDescriptorsService, + aspectProperties: PropertyDescriptorLoaderService, + appConfigService: AppConfigService, + logService: LogService, + node: MinimalNodeEntryEntity, + testPresets: any; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers: [ + PropertyDescriptorsService, + PropertyDescriptorLoaderService, + AppConfigService, + AspectWhiteListService, + { provide: LogService, useValue: { error: () => {} }}, + AlfrescoApiService + ] + }).compileComponents(); + })); + + beforeEach(() => { + contentMetadataService = TestBed.get(PropertyDescriptorsService); + aspectProperties = TestBed.get(PropertyDescriptorLoaderService); + appConfigService = TestBed.get(AppConfigService); + logService = TestBed.get(LogService); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + describe('getAspects', () => { + + beforeEach(() => { + node = { aspectNames: [ 'exif:exif', 'cm:content', 'custom:custom' ] }; + + testPresets = {}; + appConfigService.config['content-metadata'] = { + presets: testPresets + }; + }); + + it('should call the aspect properties loading for the default aspects related to the given node and defined in the app config', () => { + spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({})); + testPresets.default = { 'exif:exif': [], 'custom:custom': [], 'banana:banana': [] }; + + contentMetadataService.getAspects(node); + + expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'custom:custom']); + }); + + it('should call the aspect properties loading for the defined aspects related to the given node and defined in the app config', () => { + spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({})); + testPresets.pink = { 'cm:content': [], 'custom:custom': [] }; + + contentMetadataService.getAspects(node, 'pink'); + + expect(aspectProperties.load).toHaveBeenCalledWith(['cm:content', 'custom:custom']); + }); + + it('should call the aspect properties loading for all the node aspectNames if the "*" widecard is used for the preset', () => { + spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({})); + testPresets.default = '*'; + + contentMetadataService.getAspects(node); + + expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']); + }); + + it('should call the aspect properties loading for all the node aspectNames if there is no preset data defined in the app config', () => { + spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({})); + spyOn(logService, 'error').and.stub(); + appConfigService.config['content-metadata'] = undefined; + + contentMetadataService.getAspects(node); + + expect(logService.error).not.toHaveBeenCalled(); + expect(aspectProperties.load).toHaveBeenCalledWith(['exif:exif', 'cm:content', 'custom:custom']); + }); + + it('should show meaningful error when invalid preset are given', () => { + spyOn(aspectProperties, 'load').and.callFake(x => Observable.of({})); + spyOn(logService, 'error').and.stub(); + testPresets.pink = { 'cm:content': {}, 'custom:custom': {} }; + + contentMetadataService.getAspects(node, 'blue'); + + expect(logService.error).toHaveBeenCalledWith('No content-metadata preset for: blue'); + }); + + it('should filter out properties which are not defined in the particular aspect', () => { + spyOn(aspectProperties, 'load').and.callFake(() => { + return Observable.of([ + { + name: 'exif:exif', + properties: [ + { name: 'exif:1' }, + { name: 'exif:2' } + ] + } + ]); + }); + + testPresets.default = { 'exif:exif': ['exif:2'] }; + + contentMetadataService.getAspects(node).subscribe({ + next: (aspects) => { + expect(aspects[0].name).toBe('exif:exif'); + expect(aspects[0].properties).toContain( { name: 'exif:2' }); + expect(aspects[0].properties).not.toContain( { name: 'exif:1' }); + } + }); + }); + + it('should accept "*" wildcard for aspect properties', () => { + spyOn(aspectProperties, 'load').and.callFake(() => { + return Observable.of([ + { + name: 'exif:exif', + properties: [ + { name: 'exif:1' }, + { name: 'exif:2' } + ] + }, + { + name: 'custom:custom', + properties: [ + { name: 'custom:1' }, + { name: 'custom:2' } + ] + } + ]); + }); + + testPresets.default = { + 'exif:exif': '*', + 'custom:custom': ['custom:1'] + }; + + contentMetadataService.getAspects(node).subscribe({ + next: (aspects) => { + expect(aspects.length).toBe(2); + expect(aspects[0].name).toBe('exif:exif'); + expect(aspects[0].properties).toContain( { name: 'exif:1' }); + expect(aspects[0].properties).toContain( { name: 'exif:2' }); + + expect(aspects[1].name).toBe('custom:custom'); + expect(aspects[1].properties).toContain( { name: 'custom:1' }); + expect(aspects[1].properties).not.toContain( { name: 'custom:2' }); + } + }); + }); + + it('should filter out aspects which are not present in app config preset', () => { + spyOn(aspectProperties, 'load').and.callFake(() => { + return Observable.of([ + { + name: 'exif:exif', + properties: [ + { name: 'exif:1' } + ] + } + ]); + }); + + testPresets.default = { + 'exif:exif': ['exif:1'], + 'banana:banana': ['banana:1'] + }; + + contentMetadataService.getAspects(node).subscribe({ + next: (aspects) => { + expect(aspects.length).toBe(1); + expect(aspects[0].name).toBe('exif:exif'); + expect(aspects[0].properties).toContain( { name: 'exif:1' }); + } + }); + }); + }); +}); diff --git a/lib/content-services/content-metadata/services/property-descriptors.service.ts b/lib/content-services/content-metadata/services/property-descriptors.service.ts new file mode 100644 index 0000000000..1a24659a78 --- /dev/null +++ b/lib/content-services/content-metadata/services/property-descriptors.service.ts @@ -0,0 +1,60 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Injectable } from '@angular/core'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { PropertyDescriptorLoaderService } from './properties-loader.service'; +import { AspectWhiteListService } from './aspect-whitelist.service'; +import { Observable } from 'rxjs/Observable'; +import { Aspect, AspectProperty } from '../interfaces/content-metadata.interfaces'; + +@Injectable() +export class PropertyDescriptorsService { + + constructor(private aspectWhiteListService: AspectWhiteListService, + private aspectPropertiesService: PropertyDescriptorLoaderService) {} + + getAspects(node: MinimalNodeEntryEntity, presetName: string = 'default'): Observable { + this.aspectWhiteListService.choosePreset(presetName); + + return this.loadAspectDescriptors(node.aspectNames) + .map(this.filterPropertiesByWhitelist.bind(this)); + } + + private loadAspectDescriptors(aspectsAssignedToNode: string[]): Observable { + const aspectsToLoad = aspectsAssignedToNode + .filter(nodeAspectName => this.aspectWhiteListService.isAspectAllowed(nodeAspectName)); + + return this.aspectPropertiesService.load(aspectsToLoad); + } + + private filterPropertiesByWhitelist(aspectDescriptors): Aspect[] { + return aspectDescriptors.map((aspectDescriptor) => { + return Object.assign({}, aspectDescriptor, { + properties: this.getFilteredPropertiesArray(aspectDescriptor) + }); + }); + } + + private getFilteredPropertiesArray(aspectDescriptor): AspectProperty[] { + const aspectName = aspectDescriptor.name; + + return Object.keys(aspectDescriptor.properties) + .map(propertyName => aspectDescriptor.properties[propertyName]) + .filter(property => this.aspectWhiteListService.isPropertyAllowed(aspectName, property.name)); + } +} diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts new file mode 100644 index 0000000000..dbc2adc35f --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-dialog.service.spec.ts @@ -0,0 +1,130 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; +import { AppConfigService, SitesService } from '@alfresco/adf-core'; +import { DocumentListService } from '../document-list/services/document-list.service'; +import { ContentNodeDialogService } from './content-node-dialog.service'; +import { MatDialog } from '@angular/material'; +import { Observable } from 'rxjs/Observable'; + +const fakeNode: MinimalNodeEntryEntity = { + id: 'fake', + name: 'fake-name' +}; + +const fakeSiteList: SitePaging = { + list: { + pagination: { + count: 1, + hasMoreItems: false, + totalItems: 1, + skipCount: 0, + maxItems: 100 + }, + entries: [ + { + entry: { + id: 'FAKE', + guid: 'FAKE-GUID', + title: 'FAKE-SITE-TITLE' + } + } + ] + } +}; + +describe('ContentNodeDialogService', () => { + + let service: ContentNodeDialogService; + let documentListService: DocumentListService; + let sitesService: SitesService; + let materialDialog: MatDialog; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + ContentNodeDialogService, + DocumentListService, + SitesService, + MatDialog + ] + }).compileComponents(); + })); + + beforeEach(() => { + let appConfig: AppConfigService = TestBed.get(AppConfigService); + appConfig.config.ecmHost = 'http://localhost:9876/ecm'; + + service = TestBed.get(ContentNodeDialogService); + documentListService = TestBed.get(DocumentListService); + materialDialog = TestBed.get(MatDialog); + sitesService = TestBed.get(SitesService); + spyOn(materialDialog, 'open').and.stub(); + spyOn(materialDialog, 'closeAll').and.stub(); + + }); + + it('should be able to create the service', () => { + expect(service).not.toBeNull(); + }); + + it('should be able to open the dialog when node has permission', () => { + service.openCopyMoveDialog('fake-action', fakeNode, '!update'); + expect(materialDialog.open).toHaveBeenCalled(); + }); + + it('should NOT be able to open the dialog when node has NOT permission', () => { + service.openCopyMoveDialog('fake-action', fakeNode, 'noperm').subscribe( + () => { }, + (error) => { + expect(materialDialog.open).not.toHaveBeenCalled(); + expect(JSON.parse(error.message).error.statusCode).toBe(403); + }); + }); + + it('should be able to open the dialog using a folder id', fakeAsync(() => { + spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(fakeNode)); + service.openFileBrowseDialogByFolderId('fake-folder-id').subscribe(); + tick(); + expect(materialDialog.open).toHaveBeenCalled(); + })); + + it('should be able to open the dialog for files using the first user site', fakeAsync(() => { + spyOn(sitesService, 'getSites').and.returnValue(Observable.of(fakeSiteList)); + spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(fakeNode)); + service.openFileBrowseDialogBySite().subscribe(); + tick(); + expect(materialDialog.open).toHaveBeenCalled(); + })); + + it('should be able to open the dialog for folder using the first user site', fakeAsync(() => { + spyOn(sitesService, 'getSites').and.returnValue(Observable.of(fakeSiteList)); + spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(fakeNode)); + service.openFolderBrowseDialogBySite().subscribe(); + tick(); + expect(materialDialog.open).toHaveBeenCalled(); + })); + + it('should be able to close the material dialog', () => { + service.close(); + expect(materialDialog.closeAll).toHaveBeenCalled(); + }); + +}); diff --git a/lib/content-services/content-node-selector/content-node-dialog.service.ts b/lib/content-services/content-node-selector/content-node-dialog.service.ts new file mode 100644 index 0000000000..c6da1fe915 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-dialog.service.ts @@ -0,0 +1,173 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { MatDialog } from '@angular/material'; +import { Injectable } from '@angular/core'; +import { ContentService } from '@alfresco/adf-core'; +import { Subject } from 'rxjs/Subject'; +import { Observable } from 'rxjs/Observable'; +import { ShareDataRow } from '../document-list/data/share-data-row.model'; +import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; +import { DataColumn, SitesService } from '@alfresco/adf-core'; +import { DocumentListService } from '../document-list/services/document-list.service'; +import { ContentNodeSelectorComponent } from './content-node-selector.component'; +import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface'; + +@Injectable() +export class ContentNodeDialogService { + + constructor(private dialog: MatDialog, + private contentService: ContentService, + private documentListService: DocumentListService, + private siteService: SitesService) { } + + openFileBrowseDialogByFolderId(folderNodeId: string): Observable { + return Observable.fromPromise(this.documentListService.getFolderNode(folderNodeId)) + .switchMap((node: MinimalNodeEntryEntity) => { + return this.openUploadFileDialog('Choose', node); + }); + } + + openFileBrowseDialogBySite(): Observable { + return this.siteService.getSites().switchMap((response: SitePaging) => { + return this.openFileBrowseDialogByFolderId(response.list.entries[0].entry.guid); + }); + } + + openFolderBrowseDialogBySite(): Observable { + return this.siteService.getSites().switchMap((response: SitePaging) => { + return this.openFolderBrowseDialogByFolderId(response.list.entries[0].entry.guid); + }); + } + + openFolderBrowseDialogByFolderId(folderNodeId: string): Observable { + return Observable.fromPromise(this.documentListService.getFolderNode(folderNodeId)) + .switchMap((node: MinimalNodeEntryEntity) => { + return this.openUploadFolderDialog('Choose', node); + }); + } + + openCopyMoveDialog(action: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Observable { + if (this.contentService.hasPermission(contentEntry, permission)) { + + const select = new Subject(); + select.subscribe({ + complete: this.close.bind(this) + }); + + const data: ContentNodeSelectorComponentData = { + title: `${action} '${contentEntry.name}' to ...`, + actionName: action, + currentFolderId: contentEntry.parentId, + imageResolver: this.imageResolver.bind(this), + rowFilter : this.rowFilter.bind(this, contentEntry.id), + isSelectionValid: this.hasEntityCreatePermission.bind(this), + select: select + }; + + this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); + + return select; + } else { + let errors = new Error(JSON.stringify({ error: { statusCode: 403 } } )); + return Observable.throw(errors); + } + } + + openUploadFolderDialog(action: string, contentEntry: MinimalNodeEntryEntity): Observable { + const select = new Subject(); + select.subscribe({ + complete: this.close.bind(this) + }); + + const data: ContentNodeSelectorComponentData = { + title: `${action} '${contentEntry.name}' to ...`, + actionName: action, + currentFolderId: contentEntry.id, + imageResolver: this.imageResolver.bind(this), + isSelectionValid: this.hasPermissionOnNodeFolder.bind(this), + rowFilter : this.rowFilter.bind(this, contentEntry.id), + select: select + }; + + this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); + return select; + } + + openUploadFileDialog(action: string, contentEntry: MinimalNodeEntryEntity): Observable { + const select = new Subject(); + select.subscribe({ + complete: this.close.bind(this) + }); + + const data: ContentNodeSelectorComponentData = { + title: `${action} '${contentEntry.name}' to ...`, + actionName: action, + currentFolderId: contentEntry.id, + imageResolver: this.imageResolver.bind(this), + isSelectionValid: this.isNodeFile.bind(this), + select: select + }; + + this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px'); + return select; + } + + private openContentNodeDialog(data: ContentNodeSelectorComponentData, currentPanelClass: string, chosenWidth: string) { + this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: currentPanelClass, width: chosenWidth }); + } + + private imageResolver(row: ShareDataRow, col: DataColumn): string | null { + const entry: MinimalNodeEntryEntity = row.node.entry; + if (!this.contentService.hasPermission(entry, 'create')) { + return this.documentListService.getMimeTypeIcon('disable/folder'); + } + + return null; + } + + private rowFilter(currentNodeId, row: ShareDataRow): boolean { + const node: MinimalNodeEntryEntity = row.node.entry; + + if (node.id === currentNodeId || node.isFile) { + return false; + } else { + return true; + } + } + + private isNodeFile(entry: MinimalNodeEntryEntity): boolean { + return entry.isFile; + } + + private hasPermissionOnNodeFolder(entry: MinimalNodeEntryEntity): boolean { + return this.isNodeFolder(entry) && this.contentService.hasPermission(entry, 'create'); + } + + private isNodeFolder(entry: MinimalNodeEntryEntity): boolean { + return entry.isFolder; + } + + private hasEntityCreatePermission(entry: MinimalNodeEntryEntity): boolean { + return this.contentService.hasPermission(entry, 'create'); + } + + close() { + this.dialog.closeAll(); + } + +} diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.html b/lib/content-services/content-node-selector/content-node-selector-panel.component.html new file mode 100644 index 0000000000..ef18759562 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.html @@ -0,0 +1,95 @@ +
+ + + + clear + + + search + + + + + + + + + + + + + +
+ + + +
{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}
+
+
+ + + + + + + + + + + + +
+ + + {{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }} + +
+
diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.scss b/lib/content-services/content-node-selector/content-node-selector-panel.component.scss new file mode 100644 index 0000000000..50024727fc --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.scss @@ -0,0 +1,161 @@ +@mixin adf-content-node-selector-theme($theme) { + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + .adf-content-node-selector { + + &-content { + padding-top: 0; + + &-input { + width: 100%; + + &-icon { + color: mat-color($foreground, disabled-button); + cursor: pointer; + + &:hover { + color: mat-color($foreground, base); + } + } + } + + .mat-input-underline .mat-input-ripple { + height: 1px; + transition: none; + } + + .adf-site-dropdown-container { + .mat-form-field { + display: block; + margin-bottom: 15px; + } + } + + .adf-site-dropdown-list-element { + width: 100%; + margin-bottom: 0; + + .mat-select-trigger { + font-size: 14px; + } + + } + + .adf-toolbar .mat-toolbar { + border-bottom-width: 0; + font-size: 14px; + + &.mat-toolbar-single-row { + height: auto; + } + } + + &-breadcrumb { + .adf-dropdown-breadcumb-trigger { + outline: none; + .mat-icon { + color: mat-color($foreground, base, 0.45); + + &:hover { + color: mat-color($foreground, base, 0.65); + } + } + } + + .adf-dropddown-breadcrumb-item-chevron { + color: mat-color($foreground, base, 0.45); + } + } + + &-list { + height: 200px; + overflow: auto; + border: 1px solid mat-color($foreground, base, 0.07); + + .adf-highlight { + color: mat-color($primary); + } + + .adf-data-table { + border: none; + + .adf-no-content-container { + text-align: center; + } + + thead { + display: none; + } + + .adf-data-table-cell { + padding-top: 8px; + padding-bottom: 8px; + height: 30px; + + & .adf-name-location-cell-location { + display: none; + } + + & .adf-name-location-cell-name { + padding: 0; + } + + &--image { + padding-left: 16px; + padding-right: 8px; + } + + &--text { + padding-left: 8px; + } + } + + tbody tr { + height: auto !important; + + &:first-child { + .adf-data-table-cell { + border-top: none; + } + } + + &:last-child { + .adf-data-table-cell { + border-bottom: none; + } + } + } + } + + &-searchLayout { + + .adf-data-table { + .adf-data-table-cell { + height: 56px; + padding-bottom: 24px; + + & .adf-name-location-cell-location { + display: block + } + + & .adf-name-location-cell-name { + padding: 18px 0 2px 0; + } + + &.adf-content-selector-modified-cell { + display: none; + } + + &.adf-content-selector-modifier-cell { + display: none; + } + } + } + } + } + } + } + +} diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts new file mode 100644 index 0000000000..ea412d8c9b --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.spec.ts @@ -0,0 +1,687 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MinimalNodeEntryEntity, SiteEntry } from 'alfresco-js-api'; +import { + AlfrescoApiService, + ContentService, + TranslationService, + SearchService, + SitesService, + UserPreferencesService +} from '@alfresco/adf-core'; +import { DataTableModule } from '@alfresco/adf-core'; +import { Observable } from 'rxjs/Observable'; +import { Observer } from 'rxjs/Observer'; +import { MaterialModule } from '../material.module'; +import { EmptyFolderContentDirective, DocumentListComponent, DocumentListService } from '../document-list'; +import { DropdownSitesComponent } from '../site-dropdown'; +import { DropdownBreadcrumbComponent } from '../breadcrumb'; +import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component'; +import { ContentNodeSelectorService } from './content-node-selector.service'; +import { NodePaging } from 'alfresco-js-api'; + +const ONE_FOLDER_RESULT = { + list: { + entries: [ + { + entry: { + id: '123', name: 'MyFolder', isFile: false, isFolder: true, + createdByUser: { displayName: 'John Doe' }, + modifiedByUser: { displayName: 'John Doe' } + } + } + ], + pagination: { + hasMoreItems: true + } + } +}; + +describe('ContentNodeSelectorComponent', () => { + let component: ContentNodeSelectorPanelComponent; + let fixture: ComponentFixture; + let searchService: SearchService; + let searchSpy: jasmine.Spy; + + let _observer: Observer; + + function typeToSearchBox(searchTerm = 'string-to-search') { + let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]')); + searchInput.nativeElement.value = searchTerm; + component.searchInput.setValue(searchTerm); + fixture.detectChanges(); + } + + function respondWithSearchResults(result) { + _observer.next(result); + } + + function returnAlwaysTrue(entry: MinimalNodeEntryEntity) { + return true; + } + + function setupTestbed(plusProviders) { + TestBed.configureTestingModule({ + imports: [ + DataTableModule, + MaterialModule + ], + declarations: [ + DocumentListComponent, + EmptyFolderContentDirective, + DropdownSitesComponent, + DropdownBreadcrumbComponent, + ContentNodeSelectorPanelComponent + ], + providers: [ + AlfrescoApiService, + ContentService, + SearchService, + TranslationService, + DocumentListService, + SitesService, + ContentNodeSelectorService, + UserPreferencesService, + ...plusProviders + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('General component features', () => { + + beforeEach(async(() => { + setupTestbed([]); + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ContentNodeSelectorPanelComponent); + component = fixture.componentInstance; + component.debounceSearch = 0; + + searchService = TestBed.get(SearchService); + searchSpy = spyOn(searchService, 'searchByQueryBody').and.callFake(() => { + return Observable.create((observer: Observer) => { + _observer = observer; + }); + }); + }); + + describe('Parameters', () => { + + it('should trigger the select event when selection has been made', (done) => { + const expectedNode = {}; + component.select.subscribe((nodes) => { + expect(nodes.length).toBe(1); + expect(nodes[0]).toBe(expectedNode); + done(); + }); + + component.chosenNode = expectedNode; + }); + }); + + describe('Breadcrumbs', () => { + + let documentListService, + sitesService, + expectedDefaultFolderNode; + + beforeEach(() => { + expectedDefaultFolderNode = { path: { elements: [] } }; + documentListService = TestBed.get(DocumentListService); + sitesService = TestBed.get(SitesService); + spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode)); + spyOn(documentListService, 'getFolder').and.returnValue(Observable.throw('No results for test')); + spyOn(sitesService, 'getSites').and.returnValue(Observable.of({ list: { entries: [] } })); + spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve()); + component.currentFolderId = 'cat-girl-nuku-nuku'; + fixture.detectChanges(); + }); + + it('should show the breadcrumb for the currentFolderId by default', (done) => { + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); + expect(breadcrumb).not.toBeNull(); + expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode); + done(); + }); + }); + + it('should not show the breadcrumb if search was performed as last action', (done) => { + typeToSearchBox(); + fixture.detectChanges(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); + expect(breadcrumb).toBeNull(); + done(); + }); + }, 300); + + }); + + it('should show the breadcrumb again on folder navigation in the results list', (done) => { + typeToSearchBox(); + fixture.detectChanges(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + fixture.whenStable().then(() => { + fixture.detectChanges(); + component.onFolderChange(); + fixture.detectChanges(); + const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); + expect(breadcrumb).not.toBeNull(); + done(); + }); + }, 300); + + }); + + it('should show the breadcrumb for the selected node when search results are displayed', (done) => { + const alfrescoContentService = TestBed.get(ContentService); + spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true); + + typeToSearchBox(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + fixture.whenStable().then(() => { + fixture.detectChanges(); + + const chosenNode = { path: { elements: ['one'] } }; + component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); + fixture.detectChanges(); + + const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); + expect(breadcrumb).not.toBeNull(); + expect(breadcrumb.componentInstance.folderNode.path).toBe(chosenNode.path); + done(); + }); + }, 300); + }); + + it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => { + const alfrescoContentService = TestBed.get(ContentService); + spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true); + + typeToSearchBox(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.detectChanges(); + component.onFolderChange(); + fixture.detectChanges(); + + const chosenNode = { path: { elements: [] } }; + component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); + fixture.detectChanges(); + + const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); + expect(breadcrumb).not.toBeNull(); + expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode); + done(); + }, 300); + }); + }); + + describe('Search functionality', () => { + + function defaultSearchOptions(searchTerm, rootNodeId = undefined, skipCount = 0) { + + const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` }] : []; + + let defaultSearchNode: any = { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: '25', + skipCount: skipCount.toString() + }, + filterQueries: [ + { query: "TYPE:'cm:folder'" }, + { query: 'NOT cm:creator:System' }, + ...parentFiltering + ], + scope: { + locations: ['nodes'] + } + }; + + return defaultSearchNode; + } + + beforeEach(() => { + const documentListService = TestBed.get(DocumentListService); + const expectedDefaultFolderNode = { path: { elements: [] } }; + + spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode)); + spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve()); + + component.currentFolderId = 'cat-girl-nuku-nuku'; + fixture.detectChanges(); + }); + + it('should load the results by calling the search api on search change', (done) => { + typeToSearchBox('kakarot'); + + setTimeout(() => { + expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot')); + done(); + }, 300); + }); + + it('should reset the currently chosen node in case of starting a new search', (done) => { + component.chosenNode = {}; + typeToSearchBox('kakarot'); + + setTimeout(() => { + expect(component.chosenNode).toBeNull(); + done(); + }, 300); + }); + + it('should call the search api on changing the site selectbox\'s value', (done) => { + typeToSearchBox('vegeta'); + + setTimeout(() => { + expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search'); + + component.siteChanged( { entry: { guid: 'namek' } }); + + expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change'); + expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')] ); + done(); + }, 300); + }); + + it('should show the search icon by default without the X (clear) icon', (done) => { + fixture.detectChanges(); + setTimeout(() => { + + let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]')); + let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); + + expect(searchIcon).not.toBeNull('Search icon should be in the DOM'); + expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM'); + done(); + }, 300); + }); + + it('should show the X (clear) icon without the search icon when the search contains at least one character', (done) => { + fixture.detectChanges(); + typeToSearchBox('123'); + + setTimeout(() => { + fixture.detectChanges(); + + let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]')); + let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); + + expect(searchIcon).toBeNull('Search icon should NOT be in the DOM'); + expect(clearIcon).not.toBeNull('Clear icon should be in the DOM'); + done(); + }, 300); + }); + + it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => { + component.chosenNode = {}; + component.nodes = { + list: { + entries: [{ entry: component.chosenNode }] + } + }; + component.searchTerm = 'piccolo'; + component.showingSearchResults = true; + + component.clear(); + + expect(component.searchTerm).toBe(''); + expect(component.nodes).toEqual(null); + expect(component.chosenNode).toBeNull(); + expect(component.showingSearchResults).toBeFalsy(); + }); + + it('should show the current folder\'s content instead of search results if search was not performed', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + }); + + it('should pass through the rowFilter to the documentList', () => { + const filter = () => { + }; + component.rowFilter = filter; + + fixture.detectChanges(); + + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.rowFilter).toBe(filter); + }); + + it('should pass through the imageResolver to the documentList', () => { + const resolver = () => 'piccolo'; + component.imageResolver = resolver; + + fixture.detectChanges(); + + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.imageResolver).toBe(resolver); + }); + + it('should show the result list when search was performed', (done) => { + typeToSearchBox(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.detectChanges(); + let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.currentFolderId).toBeNull(); + done(); + }, 300); + }); + + xit('should highlight the results when search was performed in the next timeframe', (done) => { + spyOn(component.highlighter, 'highlight'); + typeToSearchBox('shenron'); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + fixture.detectChanges(); + + expect(component.highlighter.highlight).not.toHaveBeenCalled(); + + setTimeout(() => { + expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron'); + }, 300); + + done(); + }, 300); + + }); + + it('should show the default text instead of result list if search was cleared', (done) => { + typeToSearchBox(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); + expect(clearButton).not.toBeNull('Clear button should be in DOM'); + clearButton.triggerEventHandler('click', {}); + fixture.detectChanges(); + + let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + done(); + }); + }, 300); + }); + + xit('should reload the original documentlist when clearing the search input', (done) => { + typeToSearchBox('shenron'); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + typeToSearchBox(''); + fixture.detectChanges(); + + setTimeout(() => { + let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + }, 300); + + done(); + }, 300); + }); + + it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => { + component.siteChanged( { entry: { guid: 'Kame-Sennin Muten Roshi' } }); + fixture.detectChanges(); + + let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi'); + + component.siteChanged( { entry: { guid: undefined } }); + fixture.detectChanges(); + + documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + + done(); + }); + + describe('Pagination "Load more" button', () => { + + it('should NOT be shown by default', () => { + fixture.detectChanges(); + const pagination = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); + expect(pagination).toBeNull(); + }); + + it('should be shown when diplaying search results', (done) => { + typeToSearchBox('shenron'); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]')); + expect(pagination).not.toBeNull(); + done(); + }); + }, 300); + }); + + it('button\'s callback should load the next batch of results by calling the search api', () => { + const skipCount = 8; + component.searchTerm = 'kakarot'; + + component.getNextPageOfSearch({ skipCount }); + + expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount)); + }); + + it('should be shown when pagination\'s hasMoreItems is true', () => { + component.pagination = { + hasMoreItems: true + }; + fixture.detectChanges(); + + const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]')); + expect(pagination).not.toBeNull(); + }); + + it('button\'s callback should load the next batch of folder results when there is no searchTerm', () => { + const skipCount = 5; + + component.searchTerm = ''; + component.pagination = { + hasMoreItems: true + }; + fixture.detectChanges(); + + component.getNextPageOfSearch({skipCount}); + fixture.detectChanges(); + expect(component.searchTerm).toBe(''); + + expect(component.infiniteScroll).toBeTruthy(); + expect(component.skipCount).toBe(skipCount); + expect(searchSpy).not.toHaveBeenCalled(); + }); + + it('should set its loading state to true after search was started', (done) => { + component.showingSearchResults = true; + component.pagination = { hasMoreItems: true }; + + typeToSearchBox('shenron'); + + setTimeout(() => { + fixture.detectChanges(); + + const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); + const paginationLoading = fixture.debugElement.query(spinnerSelector); + expect(paginationLoading).not.toBeNull(); + done(); + }, 300); + }); + + it('should set its loading state to true after search was performed', (done) => { + component.showingSearchResults = true; + component.pagination = { hasMoreItems: true }; + + typeToSearchBox('shenron'); + fixture.detectChanges(); + + setTimeout(() => { + respondWithSearchResults(ONE_FOLDER_RESULT); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); + const paginationLoading = fixture.debugElement.query(spinnerSelector); + expect(paginationLoading).toBeNull(); + done(); + }); + }, 300); + }); + }); + }); + + describe('Action button for the chosen node', () => { + + const entry: MinimalNodeEntryEntity = { list: {entries: [{}]}}; + const nodePage: NodePaging = {list: {}, pagination: {}}; + let hasPermission; + + beforeEach(() => { + const alfrescoContentService = TestBed.get(ContentService); + spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission); + component.isSelectionValid = returnAlwaysTrue.bind(this); + }); + + it('should become enabled after loading node with the necessary permissions', async(() => { + hasPermission = true; + component.documentList.folderNode = entry; + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + component.documentList.ready.emit(nodePage); + fixture.detectChanges(); + })); + + it('should remain disabled after loading node without the necessary permissions', () => { + hasPermission = false; + component.documentList.folderNode = entry; + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + component.documentList.ready.emit(nodePage); + fixture.detectChanges(); + }); + + it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => { + hasPermission = true; + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + component.onNodeSelect({ detail: { node: { entry } } }); + fixture.detectChanges(); + }); + + it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => { + hasPermission = false; + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + component.onNodeSelect({ detail: { node: { entry } } }); + fixture.detectChanges(); + }); + + it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => { + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).not.toBeNull(); + }); + + hasPermission = true; + component.onNodeSelect({ detail: { node: { entry } } }); + fixture.detectChanges(); + + hasPermission = false; + component.onNodeSelect({ detail: { node: { entry } } }); + fixture.detectChanges(); + }); + + it('should emit null when the chosenNode is reset', () => { + hasPermission = true; + component.onNodeSelect({ detail: { node: { entry: {} } } }); + fixture.detectChanges(); + + component.select.subscribe((nodes) => { + expect(nodes).toBeDefined(); + expect(nodes).toBeNull(); + }); + + component.resetChosenNode(); + fixture.detectChanges(); + }); + }); + }); +}); diff --git a/lib/content-services/content-node-selector/content-node-selector-panel.component.ts b/lib/content-services/content-node-selector/content-node-selector-panel.component.ts new file mode 100644 index 0000000000..1a512375f4 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector-panel.component.ts @@ -0,0 +1,318 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + AlfrescoApiService, + HighlightDirective, + UserPreferencesService +} from '@alfresco/adf-core'; +import { FormControl } from '@angular/forms'; +import { MinimalNodeEntryEntity, NodePaging, Pagination, SiteEntry, SitePaging } from 'alfresco-js-api'; +import { DocumentListComponent, PaginationStrategy } from '../document-list/components/document-list.component'; +import { RowFilter } from '../document-list/data/row-filter.model'; +import { ImageResolver } from '../document-list/data/image-resolver.model'; +import { ContentNodeSelectorService } from './content-node-selector.service'; +import { debounceTime } from 'rxjs/operators'; + +export type ValidationFunction = (entry: MinimalNodeEntryEntity) => boolean; + +const defaultValidation = () => true; + +@Component({ + selector: 'adf-content-node-selector-panel', + styleUrls: ['./content-node-selector-panel.component.scss'], + templateUrl: './content-node-selector-panel.component.html', + encapsulation: ViewEncapsulation.None +}) +export class ContentNodeSelectorPanelComponent implements OnInit { + + @Input() + currentFolderId: string = null; + + @Input() + dropdownHideMyFiles: boolean = false; + + @Input() + dropdownSiteList: SitePaging = null; + + @Input() + rowFilter: RowFilter = null; + + @Input() + imageResolver: ImageResolver = null; + + @Input() + pageSize: number; + + @Input() + isSelectionValid: ValidationFunction = defaultValidation; + + @Output() + select: EventEmitter = new EventEmitter(); + + @ViewChild(DocumentListComponent) + documentList: DocumentListComponent; + + @ViewChild(HighlightDirective) + highlighter: HighlightDirective; + + nodes: NodePaging | null = null; + siteId: null | string; + searchTerm: string = ''; + showingSearchResults: boolean = false; + loadingSearchResults: boolean = false; + inDialog: boolean = false; + _chosenNode: MinimalNodeEntryEntity = null; + folderIdToShow: string | null = null; + paginationStrategy: PaginationStrategy; + pagination: Pagination; + skipCount: number = 0; + infiniteScroll: boolean = false; + debounceSearch: number= 200; + searchInput: FormControl = new FormControl(); + + constructor(private contentNodeSelectorService: ContentNodeSelectorService, + private apiService: AlfrescoApiService, + private preferences: UserPreferencesService) { + this.searchInput.valueChanges + .pipe( + debounceTime(this.debounceSearch) + ) + .subscribe((searchValue) => { + this.search(searchValue); + }); + this.pageSize = this.preferences.paginationSize; + } + + set chosenNode(value: MinimalNodeEntryEntity) { + this._chosenNode = value; + let valuesArray = null; + if (value) { + valuesArray = [value]; + } + this.select.next(valuesArray); + } + + get chosenNode() { + return this._chosenNode; + } + + ngOnInit() { + this.folderIdToShow = this.currentFolderId; + this.paginationStrategy = PaginationStrategy.Infinite; + } + + /** + * Updates the site attribute and starts a new search + * + * @param chosenSite SiteEntry to search within + */ + siteChanged(chosenSite: SiteEntry): void { + this.siteId = chosenSite.entry.guid; + this.updateResults(); + } + + /** + * Updates the searchTerm attribute and starts a new search + * + * @param searchTerm string value to search against + */ + search(searchTerm: string): void { + this.searchTerm = searchTerm; + this.updateResults(); + } + + /** + * Returns whether breadcrumb has to be shown or not + */ + needBreadcrumbs() { + const whenInFolderNavigation = !this.showingSearchResults, + whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode; + + return whenInFolderNavigation || whenInSelectingSearchResult; + } + + /** + * Returns the actually selected|entered folder node or null in case of searching for the breadcrumb + */ + get breadcrumbFolderNode(): MinimalNodeEntryEntity | null { + if (this.showingSearchResults && this.chosenNode) { + return this.chosenNode; + } else { + return this.documentList.folderNode; + } + } + + /** + * Clear the search input + */ + clear(): void { + this.searchTerm = ''; + this.nodes = null; + this.skipCount = 0; + this.chosenNode = null; + this.showingSearchResults = false; + this.folderIdToShow = this.currentFolderId; + } + + /** + * Update the result list depending on the criterias + */ + private updateResults(): void { + if (this.searchTerm.length === 0) { + this.folderIdToShow = this.siteId || this.currentFolderId; + } else { + this.startNewSearch(); + } + } + + /** + * Load the first page of a new search result + */ + private startNewSearch(): void { + this.nodes = null; + this.skipCount = 0; + this.chosenNode = null; + this.folderIdToShow = null; + this.querySearch(); + } + + /** + * Loads the next batch of search results + * + * @param event Pagination object + */ + getNextPageOfSearch(event: Pagination): void { + this.infiniteScroll = true; + this.skipCount = event.skipCount; + + if (this.searchTerm.length > 0) { + this.querySearch(); + } + } + + /** + * Perform the call to searchService with the proper parameters + */ + private querySearch(): void { + this.loadingSearchResults = true; + + this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize) + .subscribe(this.showSearchResults.bind(this)); + } + + /** + * Show the results of the search + * + * @param results Search results + */ + private showSearchResults(results: NodePaging): void { + this.showingSearchResults = true; + this.loadingSearchResults = false; + + // Documentlist hack, since data displaying for preloaded nodes is a little bit messy there + if (!this.nodes) { + this.nodes = results; + } else { + this.documentList.data.loadPage(results, true); + } + + this.pagination = results.list.pagination; + this.highlight(); + } + + /** + * Hightlight the actual searchterm in the next frame + */ + highlight(): void { + setTimeout(() => { + this.highlighter.highlight(this.searchTerm); + }, 0); + } + + /** + * Sets showingSearchResults state to be able to differentiate between search results or folder results + */ + onFolderChange(): void { + this.skipCount = 0; + this.infiniteScroll = false; + this.showingSearchResults = false; + } + + /** + * Attempts to set the currently loaded node + */ + onFolderLoaded(nodePage: NodePaging): void { + this.attemptNodeSelection(this.documentList.folderNode); + this.pagination = nodePage.list.pagination; + } + + /** + * Selects node as chosen if it has the right permission, clears the selection otherwise + * + * @param entry + */ + private attemptNodeSelection(entry: MinimalNodeEntryEntity): void { + if (this.isSelectionValid(entry)) { + this.chosenNode = entry; + } else { + this.resetChosenNode(); + } + } + + /** + * Clears the chosen node + */ + resetChosenNode(): void { + this.chosenNode = null; + } + + /** + * Invoked when user selects a node + * + * @param event CustomEvent for node-select + */ + onNodeSelect(event: any): void { + this.attemptNodeSelection(event.detail.node.entry); + } + + onNodeDoubleClick(e: CustomEvent) { + const node: any = e.detail.node.entry; + + if (node && node.guid) { + const options = { + maxItems: this.pageSize, + skipCount: this.skipCount, + include: ['path', 'properties', 'allowableOperations'] + }; + + this.apiService.nodesApi.getNode(node.guid, options) + .then(documentLibrary => { + this.documentList.performCustomSourceNavigation(documentLibrary); + }); + } + } +} diff --git a/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts b/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts index 66aaff996a..e97dccf832 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component-data.interface.ts @@ -15,16 +15,17 @@ * limitations under the License. */ -import { EventEmitter } from '@angular/core'; -import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; +import { Subject } from 'rxjs/Subject'; export interface ContentNodeSelectorComponentData { title: string; actionName?: string; - currentFolderId?: string; + currentFolderId: string; dropdownHideMyFiles?: boolean; - dropdownSiteList?: any[]; + dropdownSiteList?: SitePaging; rowFilter?: any; imageResolver?: any; - select: EventEmitter; + isSelectionValid?: (entry: MinimalNodeEntryEntity) => boolean; + select: Subject; } diff --git a/lib/content-services/content-node-selector/content-node-selector.component.html b/lib/content-services/content-node-selector/content-node-selector.component.html index 077490330d..5473e7eb2f 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.html +++ b/lib/content-services/content-node-selector/content-node-selector.component.html @@ -1,107 +1,35 @@
{{title}} + class="adf-content-node-selector-dialog-title" + data-automation-id="content-node-selector-title">{{title || data?.title}}
- - - - - clear - - - search - - - - - - - - - - - - - -
- - - -
{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}
-
-
-
- - - {{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }} - -
- + class="adf-content-node-selector-dialog-content"> + +
- -
+ diff --git a/lib/content-services/content-node-selector/content-node-selector.component.scss b/lib/content-services/content-node-selector/content-node-selector.component.scss index e4de262546..c00ce02870 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.scss +++ b/lib/content-services/content-node-selector/content-node-selector.component.scss @@ -1,147 +1,56 @@ -@mixin adf-content-node-selector-theme($theme) { +@mixin adf-content-node-selector-dialog-theme($theme) { $primary: map-get($theme, primary); $foreground: map-get($theme, foreground); $background: map-get($theme, background); - .adf-content-node-selector-dialog { + .mat-dialog-container { + padding: 0; + } - .mat-dialog-container { - padding: 0; + .adf-content-node-selector-dialog { + &-title, + &-content, + &-actions { + padding: 16px; + margin: 0; } - .adf-content-node-selector { - &-title, - &-content, - &-actions { - padding: 16px; - margin: 0; - } + &-content{ + padding-top: 0; + } - &-title::first-letter { + &-title::first-letter { + text-transform: uppercase; + } + + &-actions { + padding: 8px; + background-color: mat-color($background, background); + display: flex; + justify-content: flex-end; + color: mat-color($foreground, secondary-text); + + button { text-transform: uppercase; } - &-content { - padding-top: 0; - - &-input { - width: 100%; - - &-icon { - color: mat-color($foreground, disabled-button); - cursor: pointer; - - &:hover { - color: mat-color($foreground, base); - } - } - } - - .mat-input-underline .mat-input-ripple { - height: 1px; - transition: none; - } - - .adf-site-dropdown-container { - .mat-form-field { - display: block; - margin-bottom: 15px; - } - } - - .adf-site-dropdown-list-element { - width: 100%; - margin-bottom: 0; - - .mat-select-trigger { - font-size: 14px; - } - - } - - .adf-toolbar .mat-toolbar { - border-bottom-width: 0; - font-size: 14px; - } - - &-list { - height: 200px; - overflow: auto; - border: 1px solid mat-color($foreground, base, 0.07); - - .adf-highlight { - color: mat-color($primary); - } - - .adf-data-table { - border: none; - - .adf-no-content-container { - text-align: center; - } - - thead { - display: none; - } - - .adf-data-table-cell { - padding-top: 8px; - padding-bottom: 8px; - border-top: none; - height: 30px; - - &--image { - padding-left: 16px; - padding-right: 8px; - } - - &--text { - padding-left: 8px; - } - } - - tbody tr { - height: auto !important; - - &:last-child { - .adf-data-table-cell { - border-bottom: none; - } - } - } - } - } + &:last-child { + margin-bottom: 0px; } - &-actions { - padding: 8px; - background-color: mat-color($background, background); - display: flex; - justify-content: flex-end; - color: mat-color($foreground, secondary-text); + &-cancel { + font-weight: normal; + } - button { - text-transform: uppercase; + &-choose { + font-weight: normal; + + &[disabled] { + opacity: 0.6; } - &:last-child { - margin-bottom: 0px; - } - - &-cancel { - font-weight: normal; - } - - &-choose { - font-weight: normal; - - &[disabled] { - opacity: 0.6; - } - - &:enabled { - color: mat-color($primary); - } + &:enabled { + color: mat-color($primary); } } } diff --git a/lib/content-services/content-node-selector/content-node-selector.component.spec.ts b/lib/content-services/content-node-selector/content-node-selector.component.spec.ts index 0b5a67d8ba..31be93bfeb 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.spec.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component.spec.ts @@ -15,92 +15,48 @@ * limitations under the License. */ -import { CUSTOM_ELEMENTS_SCHEMA, EventEmitter } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { EventEmitter } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { By } from '@angular/platform-browser'; -import { MinimalNodeEntryEntity } from 'alfresco-js-api'; -import { - AlfrescoApiService, - ContentService, - TranslationService, - SearchService, - SiteModel, - SitesService, - UserPreferencesService -} from '@alfresco/adf-core'; -import { DataTableModule } from '@alfresco/adf-core'; -import { Observable } from 'rxjs/Observable'; -import { Observer } from 'rxjs/Observer'; -import { MaterialModule } from '../material.module'; -import { EmptyFolderContentDirective, DocumentListComponent, DocumentListService } from '../document-list'; -import { DropdownSitesComponent } from '../site-dropdown'; -import { DropdownBreadcrumbComponent } from '../breadcrumb'; import { ContentNodeSelectorComponent } from './content-node-selector.component'; +import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component'; import { ContentNodeSelectorService } from './content-node-selector.service'; -import { NodePaging } from 'alfresco-js-api'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { MaterialModule } from '../material.module'; +import { By } from '@angular/platform-browser'; +import { + EmptyFolderContentDirective, + DocumentListComponent, + DocumentListService + } from '../document-list'; +import { AlfrescoApiService, ContentService } from '@alfresco/adf-core'; -const ONE_FOLDER_RESULT = { - list: { - entries: [ - { - entry: { - id: '123', name: 'MyFolder', isFile: false, isFolder: true, - createdByUser: { displayName: 'John Doe' }, - modifiedByUser: { displayName: 'John Doe' } - } - } - ], - pagination: { - hasMoreItems: true - } - } -}; +describe('ContentNodeSelectorDialogComponent', () => { -describe('ContentNodeSelectorComponent', () => { let component: ContentNodeSelectorComponent; let fixture: ComponentFixture; let data: any; - let searchService: SearchService; - let searchSpy: jasmine.Spy; - let apiService: AlfrescoApiService; - let nodesApi; - - let _observer: Observer; - - function typeToSearchBox(searchTerm = 'string-to-search') { - let searchInput = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-input"]')); - searchInput.nativeElement.value = searchTerm; - component.searchInput.setValue(searchTerm); - fixture.detectChanges(); - } - - function respondWithSearchResults(result) { - _observer.next(result); - } function setupTestbed(plusProviders) { TestBed.configureTestingModule({ imports: [ - DataTableModule, MaterialModule ], declarations: [ + ContentNodeSelectorComponent, + ContentNodeSelectorPanelComponent, DocumentListComponent, - EmptyFolderContentDirective, - DropdownSitesComponent, - DropdownBreadcrumbComponent, - ContentNodeSelectorComponent + EmptyFolderContentDirective ], providers: [ + ContentNodeSelectorService, + ContentNodeSelectorPanelComponent, + DocumentListService, AlfrescoApiService, ContentService, - SearchService, - TranslationService, DocumentListService, - SitesService, ContentNodeSelectorService, - UserPreferencesService, ...plusProviders ], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -112,681 +68,100 @@ describe('ContentNodeSelectorComponent', () => { TestBed.resetTestingModule(); }); - describe('Dialog features', () => { + beforeEach(async(() => { + data = { + title: 'Move along citizen...', + actionName: 'move', + select: new EventEmitter(), + rowFilter: () => { + }, + imageResolver: () => 'piccolo', + currentFolderId: 'cat-girl-nuku-nuku' + }; - beforeEach(async(() => { - data = { - title: 'Move along citizen...', - actionName: 'move', - select: new EventEmitter(), - rowFilter: () => { - }, - imageResolver: () => 'piccolo', - currentFolderId: 'cat-girl-nuku-nuku' - }; + setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]); + TestBed.compileComponents(); + })); - setupTestbed([{ provide: MAT_DIALOG_DATA, useValue: data }]); - TestBed.compileComponents(); - })); + beforeEach(() => { + fixture = TestBed.createComponent(ContentNodeSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(ContentNodeSelectorComponent); - component = fixture.componentInstance; + describe('Data injecting with the "Material dialog way"', () => { + + it('should show the INJECTED title', () => { + const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]')); + expect(titleElement).not.toBeNull(); + expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); + }); + + it('should have the INJECTED actionName on the name of the choose button', () => { + const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton).not.toBeNull(); + expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE'); + }); + + it('should pass through the injected currentFolderId to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); + }); + + it('should pass through the injected rowFilter to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter); + }); + + it('should pass through the injected imageResolver to the documentlist', () => { + let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); + expect(documentList).not.toBeNull('Document list should be shown'); + expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver); + }); + + }); + + describe('Cancel button', () => { + + it('should complete the data stream when user click "CANCEL"', () => { + let cancelButton; + data.select.subscribe( + () => { }, + () => { }, + () => { + cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]')); + expect(cancelButton).not.toBeNull(); + }); + + cancelButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-cancel"]')); + cancelButton.triggerEventHandler('click', {}); + }); + + it('should not be shown if dialogRef is NOT injected', () => { + const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]')); + expect(closeButton).toBeNull(); + }); + }); + + describe('Action button for the chosen node', () => { + + it('should be disabled by default', () => { fixture.detectChanges(); + + let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton.nativeElement.disabled).toBeTruthy(); }); - describe('Data injecting with the "Material dialog way"', () => { + it('should be enabled when a node is chosen', () => { + component.onSelect([{ id: 'fake' }]); + fixture.detectChanges(); - it('should show the INJECTED title', () => { - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]')); - expect(titleElement).not.toBeNull(); - expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); - }); - - it('should have the INJECTED actionName on the name of the choose button', () => { - const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton).not.toBeNull(); - expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE'); - }); - - it('should pass through the injected currentFolderId to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - }); - - it('should pass through the injected rowFilter to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.rowFilter).toBe(data.rowFilter); - }); - - it('should pass through the injected imageResolver to the documentlist', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.imageResolver).toBe(data.imageResolver); - }); - - it('should trigger the INJECTED select event when selection has been made', (done) => { - const expectedNode = {}; - data.select.subscribe((nodes) => { - expect(nodes.length).toBe(1); - expect(nodes[0]).toBe(expectedNode); - done(); - }); - - component.chosenNode = expectedNode; - component.choose(); - }); + let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); + expect(actionButton.nativeElement.disabled).toBeFalsy(); }); - describe('Cancel button', () => { - - let dummyMdDialogRef; - let fakePreference: UserPreferencesService = jasmine.createSpyObj('UserPreferencesService', ['paginationSize']); - fakePreference.paginationSize = 10; - - beforeEach(() => { - dummyMdDialogRef = > { - close: () => { - } - }; - }); - - it('should be shown if dialogRef is injected', () => { - const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef); - expect(componentInstance.inDialog).toBeTruthy(); - }); - - it('should should call the close method in the injected dialogRef', () => { - spyOn(dummyMdDialogRef, 'close'); - const componentInstance = new ContentNodeSelectorComponent(null, null, null, fakePreference, data, dummyMdDialogRef); - - componentInstance.close(); - - expect(dummyMdDialogRef.close).toHaveBeenCalled(); - }); - }); }); - describe('General component features', () => { - - beforeEach(async(() => { - setupTestbed([]); - TestBed.compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ContentNodeSelectorComponent); - component = fixture.componentInstance; - component.debounceSearch = 0; - - searchService = TestBed.get(SearchService); - searchSpy = spyOn(searchService, 'search').and.callFake(() => { - return Observable.create((observer: Observer) => { - _observer = observer; - }); - }); - - apiService = TestBed.get(AlfrescoApiService); - nodesApi = apiService.nodesApi; - - }); - - describe('Parameters', () => { - - it('should show the title', () => { - component.title = 'Move along citizen...'; - fixture.detectChanges(); - - const titleElement = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-title"]')); - expect(titleElement).not.toBeNull(); - expect(titleElement.nativeElement.innerText).toBe('Move along citizen...'); - }); - - it('should trigger the select event when selection has been made', (done) => { - const expectedNode = {}; - component.select.subscribe((nodes) => { - expect(nodes.length).toBe(1); - expect(nodes[0]).toBe(expectedNode); - done(); - }); - - component.chosenNode = expectedNode; - component.choose(); - }); - }); - - describe('Breadcrumbs', () => { - - let documentListService, - sitesService, - expectedDefaultFolderNode; - - beforeEach(() => { - expectedDefaultFolderNode = { path: { elements: [] } }; - documentListService = TestBed.get(DocumentListService); - sitesService = TestBed.get(SitesService); - spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode)); - spyOn(documentListService, 'getFolder').and.returnValue(Observable.throw('No results for test')); - spyOn(sitesService, 'getSites').and.returnValue(Observable.of([])); - spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve()); - component.currentFolderId = 'cat-girl-nuku-nuku'; - fixture.detectChanges(); - }); - - it('should show the breadcrumb for the currentFolderId by default', (done) => { - fixture.detectChanges(); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); - expect(breadcrumb).not.toBeNull(); - expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode); - done(); - }); - }); - - it('should not show the breadcrumb if search was performed as last action', (done) => { - typeToSearchBox(); - fixture.detectChanges(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); - expect(breadcrumb).toBeNull(); - done(); - }); - }, 300); - - }); - - it('should show the breadcrumb again on folder navigation in the results list', (done) => { - typeToSearchBox(); - fixture.detectChanges(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - fixture.whenStable().then(() => { - fixture.detectChanges(); - component.onFolderChange(); - fixture.detectChanges(); - const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); - expect(breadcrumb).not.toBeNull(); - done(); - }); - }, 300); - - }); - - it('should show the breadcrumb for the selected node when search results are displayed', (done) => { - const alfrescoContentService = TestBed.get(ContentService); - spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true); - - typeToSearchBox(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - fixture.whenStable().then(() => { - fixture.detectChanges(); - - const chosenNode = { path: { elements: ['one'] } }; - component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); - fixture.detectChanges(); - - const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); - expect(breadcrumb).not.toBeNull(); - expect(breadcrumb.componentInstance.folderNode.path).toBe(chosenNode.path); - done(); - }); - }, 300); - }); - - it('should NOT show the breadcrumb for the selected node when not on search results list', (done) => { - const alfrescoContentService = TestBed.get(ContentService); - spyOn(alfrescoContentService, 'hasPermission').and.returnValue(true); - - typeToSearchBox(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.detectChanges(); - component.onFolderChange(); - fixture.detectChanges(); - - const chosenNode = { path: { elements: [] } }; - component.onNodeSelect({ detail: { node: { entry: chosenNode } } }); - fixture.detectChanges(); - - const breadcrumb = fixture.debugElement.query(By.directive(DropdownBreadcrumbComponent)); - expect(breadcrumb).not.toBeNull(); - expect(breadcrumb.componentInstance.folderNode).toBe(expectedDefaultFolderNode); - done(); - }, 300); - }); - }); - - describe('Search functionality', () => { - - function defaultSearchOptions(searchTerm, rootNodeId = undefined, skipCount = 0) { - let defaultSearchNode: any = { - query: { - query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm - }, - include: ['path', 'allowableOperations'], - paging: { - maxItems: '25', - skipCount: skipCount.toString() - }, - filterQueries: [ - { query: "TYPE:'cm:folder'" }, - { query: 'NOT cm:creator:System' }] - }; - - if (rootNodeId) { - defaultSearchNode.scope = rootNodeId; - } - - return defaultSearchNode; - } - - beforeEach(() => { - const documentListService = TestBed.get(DocumentListService); - const expectedDefaultFolderNode = { path: { elements: [] } }; - - spyOn(documentListService, 'getFolderNode').and.returnValue(Promise.resolve(expectedDefaultFolderNode)); - spyOn(component.documentList, 'loadFolderNodesByFolderNodeId').and.returnValue(Promise.resolve()); - - component.currentFolderId = 'cat-girl-nuku-nuku'; - fixture.detectChanges(); - }); - - it('should load the results by calling the search api on search change', (done) => { - typeToSearchBox('kakarot'); - - setTimeout(() => { - expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot')); - done(); - }, 300); - }); - - it('should reset the currently chosen node in case of starting a new search', (done) => { - component.chosenNode = {}; - typeToSearchBox('kakarot'); - - setTimeout(() => { - expect(component.chosenNode).toBeNull(); - done(); - }, 300); - }); - - it('should call the search api on changing the site selectbox\'s value', (done) => { - typeToSearchBox('vegeta'); - - setTimeout(() => { - expect(searchSpy.calls.count()).toBe(1, 'Search count should be one after only one search'); - - component.siteChanged( { guid: 'namek' }); - - expect(searchSpy.calls.count()).toBe(2, 'Search count should be two after the site change'); - expect(searchSpy.calls.argsFor(1)).toEqual([defaultSearchOptions('vegeta', 'namek')]); - done(); - }, 300); - }); - - it('should show the search icon by default without the X (clear) icon', (done) => { - fixture.detectChanges(); - setTimeout(() => { - - let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]')); - let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); - - expect(searchIcon).not.toBeNull('Search icon should be in the DOM'); - expect(clearIcon).toBeNull('Clear icon should NOT be in the DOM'); - done(); - }, 300); - }); - - it('should show the X (clear) icon without the search icon when the search contains at least one character', (done) => { - fixture.detectChanges(); - typeToSearchBox('123'); - - setTimeout(() => { - fixture.detectChanges(); - - let searchIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-icon"]')); - let clearIcon = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); - - expect(searchIcon).toBeNull('Search icon should NOT be in the DOM'); - expect(clearIcon).not.toBeNull('Clear icon should be in the DOM'); - done(); - }, 300); - }); - - it('should clear the search field, nodes and chosenNode when clicking on the X (clear) icon', () => { - component.chosenNode = {}; - component.nodes = { - list: { - entries: [{ entry: component.chosenNode }] - } - }; - component.searchTerm = 'piccolo'; - component.showingSearchResults = true; - - component.clear(); - - expect(component.searchTerm).toBe(''); - expect(component.nodes).toEqual(null); - expect(component.chosenNode).toBeNull(); - expect(component.showingSearchResults).toBeFalsy(); - }); - - it('should show the current folder\'s content instead of search results if search was not performed', () => { - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - }); - - it('should pass through the rowFilter to the documentList', () => { - const filter = () => { - }; - component.rowFilter = filter; - - fixture.detectChanges(); - - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.rowFilter).toBe(filter); - }); - - it('should pass through the imageResolver to the documentList', () => { - const resolver = () => 'piccolo'; - component.imageResolver = resolver; - - fixture.detectChanges(); - - let documentList = fixture.debugElement.query(By.directive(DocumentListComponent)); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.imageResolver).toBe(resolver); - }); - - it('should show the result list when search was performed', (done) => { - typeToSearchBox(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.detectChanges(); - let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.currentFolderId).toBeNull(); - done(); - }, 300); - }); - - xit('should highlight the results when search was performed in the next timeframe', (done) => { - spyOn(component.highlighter, 'highlight'); - typeToSearchBox('shenron'); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - fixture.detectChanges(); - - expect(component.highlighter.highlight).not.toHaveBeenCalled(); - - setTimeout(() => { - expect(component.highlighter.highlight).toHaveBeenCalledWith('shenron'); - }, 300); - - done(); - }, 300); - - }); - - it('should show the default text instead of result list if search was cleared', (done) => { - typeToSearchBox(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - let clearButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-clear"]')); - expect(clearButton).not.toBeNull('Clear button should be in DOM'); - clearButton.triggerEventHandler('click', {}); - fixture.detectChanges(); - - let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList).not.toBeNull('Document list should be shown'); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - done(); - }); - }, 300); - }); - - xit('should reload the original documentlist when clearing the search input', (done) => { - typeToSearchBox('shenron'); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - typeToSearchBox(''); - fixture.detectChanges(); - - setTimeout(() => { - let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - }, 300); - - done(); - }, 300); - }); - - it('should set the folderIdToShow to the default "currentFolderId" if siteId is undefined', (done) => { - component.siteChanged( { guid: 'Kame-Sennin Muten Roshi' }); - fixture.detectChanges(); - - let documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList.componentInstance.currentFolderId).toBe('Kame-Sennin Muten Roshi'); - - component.siteChanged( { guid: undefined }); - fixture.detectChanges(); - - documentList = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-document-list"]')); - expect(documentList.componentInstance.currentFolderId).toBe('cat-girl-nuku-nuku'); - - done(); - }); - - describe('Pagination "Load more" button', () => { - - it('should NOT be shown by default', () => { - fixture.detectChanges(); - const pagination = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); - expect(pagination).toBeNull(); - }); - - it('should be shown when diplaying search results', (done) => { - typeToSearchBox('shenron'); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const pagination = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-search-pagination"]')); - expect(pagination).not.toBeNull(); - done(); - }); - }, 300); - }); - - it('button\'s callback should load the next batch of results by calling the search api', () => { - const skipCount = 8; - component.searchTerm = 'kakarot'; - - component.getNextPageOfSearch({ skipCount }); - - expect(searchSpy).toHaveBeenCalledWith(defaultSearchOptions('kakarot', undefined, skipCount)); - }); - - it('should set its loading state to true after search was started', (done) => { - component.showingSearchResults = true; - component.pagination = { hasMoreItems: true }; - - typeToSearchBox('shenron'); - - setTimeout(() => { - fixture.detectChanges(); - - const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); - const paginationLoading = fixture.debugElement.query(spinnerSelector); - expect(paginationLoading).not.toBeNull(); - done(); - }, 300); - }); - - it('should set its loading state to true after search was performed', (done) => { - component.showingSearchResults = true; - component.pagination = { hasMoreItems: true }; - - typeToSearchBox('shenron'); - fixture.detectChanges(); - - setTimeout(() => { - respondWithSearchResults(ONE_FOLDER_RESULT); - - fixture.whenStable().then(() => { - fixture.detectChanges(); - const spinnerSelector = By.css('[data-automation-id="content-node-selector-search-pagination"] [data-automation-id="adf-infinite-pagination-spinner"]'); - const paginationLoading = fixture.debugElement.query(spinnerSelector); - expect(paginationLoading).toBeNull(); - done(); - }); - }, 300); - }); - }); - }); - - describe('Cancel button', () => { - - it('should not be shown if dialogRef is NOT injected', () => { - const closeButton = fixture.debugElement.query(By.css('[content-node-selector-actions-cancel]')); - expect(closeButton).toBeNull(); - }); - }); - - describe('Action button for the chosen node', () => { - - const entry: MinimalNodeEntryEntity = {}; - let hasPermission; - - beforeEach(() => { - const alfrescoContentService = TestBed.get(ContentService); - spyOn(alfrescoContentService, 'hasPermission').and.callFake(() => hasPermission); - }); - - it('should be disabled by default', () => { - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should become enabled after loading node with the necessary permissions', () => { - hasPermission = true; - component.documentList.folderNode = entry; - component.documentList.ready.emit(); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(false); - }); - - it('should remain disabled after loading node without the necessary permissions', () => { - hasPermission = false; - component.documentList.folderNode = entry; - component.documentList.ready.emit(); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should be enabled when clicking on a node (with the right permissions) in the list (onNodeSelect)', () => { - hasPermission = true; - - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(false); - }); - - it('should remain disabled when clicking on a node (with the WRONG permissions) in the list (onNodeSelect)', () => { - hasPermission = false; - - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should become disabled when clicking on a node (with the WRONG permissions) after previously selecting a right node', () => { - hasPermission = true; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - hasPermission = false; - component.onNodeSelect({ detail: { node: { entry } } }); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should be disabled when resetting the chosen node', () => { - hasPermission = true; - component.onNodeSelect({ detail: { node: { entry: {} } } }); - fixture.detectChanges(); - - component.resetChosenNode(); - fixture.detectChanges(); - - let actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]')); - expect(actionButton.nativeElement.disabled).toBe(true); - }); - - it('should make the call to get the corresponding node entry to emit when a site node is selected as destination', () => { - spyOn(nodesApi, 'getNode').and.callFake((nodeId) => { - return new Promise(resolve => { - resolve({ entry: { id: nodeId } }); - }); - }); - - const siteNode1 = { title: 'my files', guid: '-my-' }; - const siteNode2 = { title: 'my sites', guid: '-mysites-' }; - - component.dropdownSiteList = [siteNode1, siteNode2]; - fixture.detectChanges(); - component.chosenNode = siteNode1; - fixture.detectChanges(); - component.choose(); - - const options = { - include: ['path', 'properties', 'allowableOperations'] - }; - expect(nodesApi.getNode).toHaveBeenCalledWith( - '-my-', - options - ); - }); - - }); - }); }); diff --git a/lib/content-services/content-node-selector/content-node-selector.component.ts b/lib/content-services/content-node-selector/content-node-selector.component.ts index 3951b0fda5..f17bc989a9 100644 --- a/lib/content-services/content-node-selector/content-node-selector.component.ts +++ b/lib/content-services/content-node-selector/content-node-selector.component.ts @@ -15,345 +15,80 @@ * limitations under the License. */ -import { - Component, - EventEmitter, - Inject, - Input, - OnInit, - Optional, - Output, - ViewChild, - ViewEncapsulation -} from '@angular/core'; -import { - AlfrescoApiService, - ContentService, - HighlightDirective, - SiteModel, - UserPreferencesService -} from '@alfresco/adf-core'; -import { FormControl } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { MinimalNodeEntryEntity, NodePaging, Pagination, Site } from 'alfresco-js-api'; -import { DocumentListComponent, PaginationStrategy } from '../document-list/components/document-list.component'; +import { Component, Inject, ViewEncapsulation, Input } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material'; +import { MinimalNodeEntryEntity, SitePaging } from 'alfresco-js-api'; +import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface'; import { RowFilter } from '../document-list/data/row-filter.model'; import { ImageResolver } from '../document-list/data/image-resolver.model'; -import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface'; -import { ContentNodeSelectorService } from './content-node-selector.service'; -import { debounceTime } from 'rxjs/operators'; - @Component({ selector: 'adf-content-node-selector', - styleUrls: ['./content-node-selector.component.scss'], templateUrl: './content-node-selector.component.html', + styleUrls: ['./content-node-selector.component.scss'], encapsulation: ViewEncapsulation.None }) -export class ContentNodeSelectorComponent implements OnInit { - - nodes: NodePaging | null = null; - siteId: null | string; - searchTerm: string = ''; - showingSearchResults: boolean = false; - loadingSearchResults: boolean = false; - inDialog: boolean = false; - chosenNode: MinimalNodeEntryEntity | Site | null = null; - folderIdToShow: string | null = null; - paginationStrategy: PaginationStrategy; - pagination: Pagination; - skipCount: number = 0; - infiniteScroll: boolean = false; - buttonActionName: string; +export class ContentNodeSelectorComponent { + /** + * @deprecated in 2.1.0 + */ @Input() - title: string; + title: string = null; + /** + * @deprecated in 2.1.0 + */ @Input() - actionName: string; - - @Input() - currentFolderId: string | null = null; + currentFolderId: string = null; + /** + * @deprecated in 2.1.0 + */ @Input() dropdownHideMyFiles: boolean = false; + /** + * @deprecated in 2.1.0 + */ @Input() - dropdownSiteList: any[] = null; + dropdownSiteList: SitePaging = null; + /** + * @deprecated in 2.1.0 + */ @Input() rowFilter: RowFilter = null; + /** + * @deprecated in 2.1.0 + */ @Input() imageResolver: ImageResolver = null; + /** + * @deprecated in 2.1.0 + */ @Input() pageSize: number; - @Output() - select: EventEmitter = new EventEmitter(); + buttonActionName: string; + private chosenNode: MinimalNodeEntryEntity[]; - @ViewChild(DocumentListComponent) - documentList: DocumentListComponent; - - @ViewChild(HighlightDirective) - highlighter: HighlightDirective; - - debounceSearch: number= 200; - - searchInput: FormControl = new FormControl(); - - constructor(private contentNodeSelectorService: ContentNodeSelectorService, - private contentService: ContentService, - private apiService: AlfrescoApiService, - private preferences: UserPreferencesService, - @Optional() @Inject(MAT_DIALOG_DATA) data?: ContentNodeSelectorComponentData, - @Optional() private containingDialog?: MatDialogRef) { - if (data) { - this.title = data.title; - this.actionName = data.actionName; - this.select = data.select; - this.currentFolderId = data.currentFolderId; - this.dropdownHideMyFiles = data.dropdownHideMyFiles; - this.dropdownSiteList = data.dropdownSiteList; - this.rowFilter = data.rowFilter; - this.imageResolver = data.imageResolver; - } - this.buttonActionName = this.actionName ? `NODE_SELECTOR.${this.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE'; - - if (this.containingDialog) { - this.inDialog = true; - } - - this.searchInput.valueChanges - .pipe( - debounceTime(this.debounceSearch) - ) - .subscribe((searchValue) => { - this.search(searchValue); - }); - - this.pageSize = this.preferences.paginationSize; + constructor(@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) { + this.buttonActionName = data.actionName ? `NODE_SELECTOR.${data.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE'; } - ngOnInit() { - this.folderIdToShow = this.currentFolderId; - this.paginationStrategy = PaginationStrategy.Infinite; + close() { + this.data.select.complete(); } - /** - * Updates the site attribute and starts a new search - * - * @param chosenSite Sitemodel to search within - */ - siteChanged(chosenSite: SiteModel): void { - this.siteId = chosenSite.guid; - this.updateResults(); + onSelect(nodeList: MinimalNodeEntryEntity[]) { + this.chosenNode = nodeList; } - /** - * Updates the searchTerm attribute and starts a new search - * - * @param searchTerm string value to search against - */ - search(searchTerm: string): void { - this.searchTerm = searchTerm; - this.updateResults(); - } - - /** - * Returns whether breadcrumb has to be shown or not - */ - needBreadcrumbs() { - const whenInFolderNavigation = !this.showingSearchResults, - whenInSelectingSearchResult = this.showingSearchResults && this.chosenNode; - - return whenInFolderNavigation || whenInSelectingSearchResult; - } - - /** - * Returns the actually selected|entered folder node or null in case of searching for the breadcrumb - */ - get breadcrumbFolderNode(): MinimalNodeEntryEntity | null { - if (this.showingSearchResults && this.chosenNode) { - return this.chosenNode; - } else { - return this.documentList.folderNode; - } - } - - /** - * Clear the search input - */ - clear(): void { - this.searchTerm = ''; - this.nodes = null; - this.skipCount = 0; - this.chosenNode = null; - this.showingSearchResults = false; - this.folderIdToShow = this.currentFolderId; - } - - /** - * Update the result list depending on the criterias - */ - private updateResults(): void { - if (this.searchTerm.length === 0) { - this.folderIdToShow = this.siteId || this.currentFolderId; - } else { - this.startNewSearch(); - } - } - - /** - * Load the first page of a new search result - */ - private startNewSearch(): void { - this.nodes = null; - this.skipCount = 0; - this.chosenNode = null; - this.folderIdToShow = null; - this.querySearch(); - } - - /** - * Loads the next batch of search results - * - * @param event Pagination object - */ - getNextPageOfSearch(event: Pagination): void { - this.infiniteScroll = true; - this.skipCount = event.skipCount; - this.querySearch(); - } - - /** - * Perform the call to searchService with the proper parameters - */ - private querySearch(): void { - this.loadingSearchResults = true; - - this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.skipCount, this.pageSize) - .subscribe(this.showSearchResults.bind(this)); - } - - /** - * Show the results of the search - * - * @param results Search results - */ - private showSearchResults(results: NodePaging): void { - this.showingSearchResults = true; - this.loadingSearchResults = false; - - // Documentlist hack, since data displaying for preloaded nodes is a little bit messy there - if (!this.nodes) { - this.nodes = results; - } else { - this.documentList.data.loadPage(results, true); - } - - this.pagination = results.list.pagination; - this.highlight(); - } - - /** - * Hightlight the actual searchterm in the next frame - */ - highlight(): void { - setTimeout(() => { - this.highlighter.highlight(this.searchTerm); - }, 0); - } - - /** - * Invoked when user selects a node - * - * @param event CustomEvent for node-select - */ - onNodeSelect(event: any): void { - this.attemptNodeSelection(event.detail.node.entry); - } - - /** - * Sets showingSearchResults state to be able to differentiate between search results or folder results - */ - onFolderChange(): void { - this.skipCount = 0; - this.infiniteScroll = false; - this.showingSearchResults = false; - } - - /** - * Attempts to set the currently loaded node - */ - onFolderLoaded(nodePage: NodePaging): void { - this.attemptNodeSelection(this.documentList.folderNode); - this.pagination = nodePage.list.pagination; - } - - /** - * Selects node as chosen if it has the right permission, clears the selection otherwise - * - * @param entry - */ - private attemptNodeSelection(entry: MinimalNodeEntryEntity): void { - if (this.contentService.hasPermission(entry, 'create')) { - this.chosenNode = entry; - } else { - this.resetChosenNode(); - } - } - - /** - * Clears the chosen node - */ - resetChosenNode(): void { - this.chosenNode = null; - } - - /** - * Emit event with the chosen node - */ - choose(): void { - const entry: any = this.chosenNode; - - if (entry && entry.guid) { - const options = { - include: ['path', 'properties', 'allowableOperations'] - }; - this.apiService.nodesApi.getNode(entry.guid, options) - .then(chosenSiteNode => { - this.select.next([chosenSiteNode.entry]); - }); - - } else { - this.select.next([this.chosenNode]); - - } - } - - /** - * Close the dialog - */ - close(): void { - this.containingDialog.close(); - } - - onNodeDoubleClick(e: CustomEvent) { - const node: any = e.detail.node.entry; - - if (node && node.guid) { - const options = { - maxItems: this.pageSize, - skipCount: this.skipCount, - include: ['path', 'properties', 'allowableOperations'] - }; - - this.apiService.nodesApi.getNode(node.guid, options) - .then(documentLibrary => { - this.documentList.performCustomSourceNavigation(documentLibrary); - }); - } + onClick(): void { + this.data.select.next(this.chosenNode); + this.data.select.complete(); } } diff --git a/lib/content-services/content-node-selector/content-node-selector.module.ts b/lib/content-services/content-node-selector/content-node-selector.module.ts index 03a3c36c22..d9c974a4bf 100644 --- a/lib/content-services/content-node-selector/content-node-selector.module.ts +++ b/lib/content-services/content-node-selector/content-node-selector.module.ts @@ -21,12 +21,15 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MaterialModule } from '../material.module'; import { TranslateModule } from '@ngx-translate/core'; +import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component'; import { ContentNodeSelectorComponent } from './content-node-selector.component'; import { ContentNodeSelectorService } from './content-node-selector.service'; +import { ContentNodeDialogService } from './content-node-dialog.service'; import { SitesDropdownModule } from '../site-dropdown/sites-dropdown.module'; import { BreadcrumbModule } from '../breadcrumb/breadcrumb.module'; -import { PaginationModule, ToolbarModule, DirectiveModule } from '@alfresco/adf-core'; +import { PaginationModule, ToolbarModule, DirectiveModule, DataColumnModule, DataTableModule } from '@alfresco/adf-core'; import { DocumentListModule } from '../document-list/document-list.module'; +import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component'; @NgModule({ imports: [ @@ -40,19 +43,24 @@ import { DocumentListModule } from '../document-list/document-list.module'; BreadcrumbModule, ToolbarModule, DocumentListModule, + DataColumnModule, + DataTableModule, PaginationModule ], exports: [ - ContentNodeSelectorComponent + ContentNodeSelectorPanelComponent, ContentNodeSelectorComponent ], entryComponents: [ - ContentNodeSelectorComponent + ContentNodeSelectorPanelComponent, ContentNodeSelectorComponent ], declarations: [ + ContentNodeSelectorPanelComponent, + NameLocationCellComponent, ContentNodeSelectorComponent ], providers: [ - ContentNodeSelectorService + ContentNodeSelectorService, + ContentNodeDialogService ] }) export class ContentNodeSelectorModule {} diff --git a/lib/content-services/content-node-selector/content-node-selector.service.spec.ts b/lib/content-services/content-node-selector/content-node-selector.service.spec.ts new file mode 100644 index 0000000000..9669449206 --- /dev/null +++ b/lib/content-services/content-node-selector/content-node-selector.service.spec.ts @@ -0,0 +1,106 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { QueryBody } from 'alfresco-js-api'; +import { SearchService } from '@alfresco/adf-core'; +import { ContentNodeSelectorService } from './content-node-selector.service'; + +class SearchServiceMock { + public query: QueryBody; + searchByQueryBody(query: QueryBody) { + this.query = query; + } +} + +describe('ContentNodeSelectorService', () => { + + let service: ContentNodeSelectorService, + search: SearchServiceMock; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + providers: [ + ContentNodeSelectorService, + { provide: SearchService, useClass: SearchServiceMock } + ] + }).compileComponents(); + })); + + beforeEach(() => { + service = TestBed.get(ContentNodeSelectorService); + search = TestBed.get(SearchService); + }); + + it('should have the proper main query for search string', () => { + service.search('nuka cola quantum'); + + expect(search.query.query).toEqual({ + query: 'nuka cola quantum* OR name:nuka cola quantum*' + }); + }); + + it('should make it including the path and allowableOperations', () => { + service.search('nuka cola quantum'); + + expect(search.query.include).toEqual(['path', 'allowableOperations']); + }); + + it('should make the search restricted to nodes only', () => { + service.search('nuka cola quantum'); + + expect(search.query.scope.locations).toEqual(['nodes']); + }); + + it('should set the maxItems and paging properly by parameters', () => { + service.search('nuka cola quantum', null, 10, 100); + + expect(search.query.paging.maxItems).toEqual('100'); + expect(search.query.paging.skipCount).toEqual('10'); + }); + + it('should set the maxItems and paging properly by default', () => { + service.search('nuka cola quantum'); + + expect(search.query.paging.maxItems).toEqual('25'); + expect(search.query.paging.skipCount).toEqual('0'); + }); + + it('should filter the search only for folders', () => { + service.search('nuka cola quantum'); + + expect(search.query.filterQueries).toContain({ query: "TYPE:'cm:folder'" }); + }); + + it('should filter out the "system-base" entries', () => { + service.search('nuka cola quantum'); + + expect(search.query.filterQueries).toContain({ query: 'NOT cm:creator:System' }); + }); + + it('should filter for the provided ancestor if defined', () => { + service.search('nuka cola quantum', 'diamond-city'); + + expect(search.query.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' }); + }); + + it('should NOT filter for the ancestor if NOT defined', () => { + service.search('nuka cola quantum'); + + expect(search.query.filterQueries).not.toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/null\'' }); + }); +}); diff --git a/lib/content-services/content-node-selector/content-node-selector.service.ts b/lib/content-services/content-node-selector/content-node-selector.service.ts index 474267c9c8..d743fbc5f7 100644 --- a/lib/content-services/content-node-selector/content-node-selector.service.ts +++ b/lib/content-services/content-node-selector/content-node-selector.service.ts @@ -26,8 +26,7 @@ import { Observable } from 'rxjs/Observable'; @Injectable() export class ContentNodeSelectorService { - constructor(private searchService: SearchService) { - } + constructor(private searchService: SearchService) {} /** * Performs a search for content node selection @@ -37,7 +36,9 @@ export class ContentNodeSelectorService { * @param rootNodeId The root is to start the search from * @param maxItems How many items to load */ - public search(searchTerm: string, rootNodeId: string, skipCount: number = 0, maxItems: number = 25): Observable { + public search(searchTerm: string, rootNodeId: string = null, skipCount: number = 0, maxItems: number = 25): Observable { + + const parentFiltering = rootNodeId ? [ { query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'` } ] : []; let defaultSearchNode: any = { query: { @@ -50,13 +51,14 @@ export class ContentNodeSelectorService { }, filterQueries: [ { query: "TYPE:'cm:folder'" }, - { query: 'NOT cm:creator:System' }] + { query: 'NOT cm:creator:System' }, + ...parentFiltering + ], + scope: { + locations: ['nodes'] + } }; - if (rootNodeId) { - defaultSearchNode.scope = rootNodeId; - } - - return this.searchService.search(defaultSearchNode); + return this.searchService.searchByQueryBody(defaultSearchNode); } } diff --git a/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.scss b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.scss new file mode 100644 index 0000000000..cb3aec5dd0 --- /dev/null +++ b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.scss @@ -0,0 +1,13 @@ +@mixin adf-name-location-cell-theme($theme) { + $foreground: map-get($theme, foreground); + + .adf-name-location-cell { + &-location { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: mat-color($foreground, base, 0.5); + font-size: 12px; + } + } +} diff --git a/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.spec.ts b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.spec.ts new file mode 100644 index 0000000000..72376c2b35 --- /dev/null +++ b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.spec.ts @@ -0,0 +1,65 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NameLocationCellComponent } from './name-location-cell.component'; +import { By } from '@angular/platform-browser'; +import { DataRow } from '@alfresco/adf-core'; + +describe('NameLocationCellComponent', () => { + let component: NameLocationCellComponent; + let fixture: ComponentFixture; + let rowData: DataRow; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + NameLocationCellComponent + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NameLocationCellComponent); + component = fixture.componentInstance; + + rowData = { + getValue(key) { + if (key === 'name') { + return 'file-name'; + } else if (key === 'path') { + return { name: '/path/to/location' }; + } + } + }; + component.row = rowData; + fixture.detectChanges(); + }); + + it('should set fileName and location properly', () => { + const fileName = fixture.debugElement.query(By.css('.adf-name-location-cell-name')).nativeElement.innerText; + const location = fixture.debugElement.query(By.css('.adf-name-location-cell-location')).nativeElement.innerText; + expect(fileName).toBe('file-name'); + expect(location).toBe('/path/to/location'); + }); + + it('should set tooltip properly', () => { + const tooltip = fixture.debugElement.query(By.css('.adf-name-location-cell-location')).nativeElement.getAttribute('title'); + + expect(tooltip).toEqual('/path/to/location'); + }); +}); diff --git a/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.ts b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.ts new file mode 100644 index 0000000000..61800cbc9d --- /dev/null +++ b/lib/content-services/content-node-selector/name-location-cell/name-location-cell.component.ts @@ -0,0 +1,43 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { DataTableCellComponent } from '@alfresco/adf-core'; + +@Component({ + selector: 'adf-name-location-cell', + template: ` +
{{ name }}
+
{{ path?.name }}
+ `, + styleUrls: ['./name-location-cell.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'adf-name-location-cell' } +}) +export class NameLocationCellComponent extends DataTableCellComponent implements OnInit { + + path: string = ''; + name: string = ''; + + ngOnInit() { + if (this.row) { + this.path = this.row.getValue('path'); + this.name = this.row.getValue('name'); + } + } +} diff --git a/lib/content-services/content-node-selector/public-api.ts b/lib/content-services/content-node-selector/public-api.ts index 585c742fdf..b3736503c3 100644 --- a/lib/content-services/content-node-selector/public-api.ts +++ b/lib/content-services/content-node-selector/public-api.ts @@ -16,5 +16,7 @@ */ export * from './content-node-selector.component-data.interface'; +export * from './content-node-selector-panel.component'; export * from './content-node-selector.component'; export * from './content-node-selector.service'; +export * from './content-node-dialog.service'; diff --git a/lib/content-services/content.module.ts b/lib/content-services/content.module.ts index 3bbace2f4f..a52cdb2f1f 100644 --- a/lib/content-services/content.module.ts +++ b/lib/content-services/content.module.ts @@ -83,8 +83,7 @@ import { ContentMetadataModule } from './content-metadata/content-metadata.modul ContentNodeSelectorModule, ContentMetadataModule, DialogModule, - FolderDirectiveModule, - MaterialModule + FolderDirectiveModule ] }) export class ContentModule { diff --git a/lib/content-services/dialogs/download-zip.dialog.ts b/lib/content-services/dialogs/download-zip.dialog.ts index ffdb2f472a..84a9b003e9 100644 --- a/lib/content-services/dialogs/download-zip.dialog.ts +++ b/lib/content-services/dialogs/download-zip.dialog.ts @@ -34,32 +34,17 @@ export class DownloadZipDialogComponent implements OnInit { constructor(private apiService: AlfrescoApiService, private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) private data: { nodeIds?: string[] }, + @Inject(MAT_DIALOG_DATA) private data: any, private logService: LogService) { } - private get downloadsApi() { - return this.apiService.getInstance().core.downloadsApi; - } - - private get nodesApi() { - return this.apiService.getInstance().core.nodesApi; - } - - private get contentApi() { - return this.apiService.getInstance().content; - } - ngOnInit() { if (this.data && this.data.nodeIds && this.data.nodeIds.length > 0) { - // change timeout to have a delay for demo purposes - setTimeout(() => { - if (!this.cancelled) { - this.downloadZip(this.data.nodeIds); - } else { - this.logService.log('Cancelled'); - } - }, 0); + if (!this.cancelled) { + this.downloadZip(this.data.nodeIds); + } else { + this.logService.log('Cancelled'); + } } } @@ -71,7 +56,7 @@ export class DownloadZipDialogComponent implements OnInit { downloadZip(nodeIds: string[]) { if (nodeIds && nodeIds.length > 0) { - const promise: any = this.downloadsApi.createDownload({ nodeIds }); + const promise: any = this.apiService.getInstance().core.downloadsApi.createDownload({ nodeIds }); promise.on('progress', progress => this.logService.log('Progress', progress)); promise.on('error', error => this.logService.error('Error', error)); @@ -79,9 +64,9 @@ export class DownloadZipDialogComponent implements OnInit { promise.on('success', (data: DownloadEntry) => { if (data && data.entry && data.entry.id) { - const url = this.contentApi.getContentUrl(data.entry.id, true); - // the call is needed only to get the name of the package - this.nodesApi.getNode(data.entry.id).then((downloadNode: MinimalNodeEntity) => { + const url = this.apiService.getInstance().content.getContentUrl(data.entry.id, true); + + this.apiService.getInstance().core.nodesApi.getNode(data.entry.id).then((downloadNode: MinimalNodeEntity) => { this.logService.log(downloadNode); const fileName = downloadNode.entry.name; this.waitAndDownload(data.entry.id, url, fileName); @@ -96,9 +81,9 @@ export class DownloadZipDialogComponent implements OnInit { return; } - this.downloadsApi.getDownload(downloadId).then((d: DownloadEntry) => { - if (d.entry) { - if (d.entry.status === 'DONE') { + this.apiService.getInstance().core.downloadsApi.getDownload(downloadId).then((downloadEntry: DownloadEntry) => { + if (downloadEntry.entry) { + if (downloadEntry.entry.status === 'DONE') { this.download(url, fileName); } else { setTimeout(() => { diff --git a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts index a22c3ff1df..f30f94f7a9 100644 --- a/lib/content-services/document-list/components/content-action/content-action.component.spec.ts +++ b/lib/content-services/document-list/components/content-action/content-action.component.spec.ts @@ -62,7 +62,7 @@ describe('ContentAction', () => { beforeEach(() => { contentService = TestBed.get(ContentService); - nodeActionsService = new NodeActionsService(null, null, null); + nodeActionsService = new NodeActionsService(null, null); documentActions = new DocumentActionsService(nodeActionsService); folderActions = new FolderActionsService(nodeActionsService, null, contentService); diff --git a/lib/content-services/document-list/components/content-action/content-action.component.ts b/lib/content-services/document-list/components/content-action/content-action.component.ts index 5361cdfad5..6c0f93fd92 100644 --- a/lib/content-services/document-list/components/content-action/content-action.component.ts +++ b/lib/content-services/document-list/components/content-action/content-action.component.ts @@ -35,36 +35,51 @@ import { ContentActionListComponent } from './content-action-list.component'; }) export class ContentActionComponent implements OnInit, OnChanges { + /** The title of the action as shown in the menu. */ @Input() title: string = 'Action'; + /** The name of the icon to display next to the menu command (can be left blank). */ @Input() icon: string; + /** System actions. Can be "delete", "download", "copy" or "move". */ @Input() handler: string; + /** Type of item that the action appies to. Can be "document" or "folder" */ @Input() target: string; + /** The permission type. */ @Input() permission: string; + /** Should this action be disabled in the menu if the user doesn't have permission for it? */ @Input() disableWithNoPermission: boolean; + /** Is the menu item disabled? */ @Input() disabled: boolean = false; + /** Emitted when the user selects the action from the menu. */ @Output() execute = new EventEmitter(); + /** Emitted when a permission error occurs */ @Output() permissionEvent = new EventEmitter(); + /** Emitted when an error occurs during the action. + * Applies to copy and move actions. + */ @Output() error = new EventEmitter(); + /** Emitted when the action succeeds with the success string message. + * Applies to copy, move and delete actions. + */ @Output() success = new EventEmitter(); diff --git a/lib/content-services/document-list/components/content-column/content-column.component.ts b/lib/content-services/document-list/components/content-column/content-column.component.ts index ebca175b37..1a79d22b91 100644 --- a/lib/content-services/document-list/components/content-column/content-column.component.ts +++ b/lib/content-services/document-list/components/content-column/content-column.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-input-rename */ import { DataColumn } from '@alfresco/adf-core'; import { LogService } from '@alfresco/adf-core'; diff --git a/lib/content-services/document-list/components/document-list.component.ts b/lib/content-services/document-list/components/document-list.component.ts index f01d57a7a6..4c6b380f77 100644 --- a/lib/content-services/document-list/components/document-list.component.ts +++ b/lib/content-services/document-list/components/document-list.component.ts @@ -41,6 +41,7 @@ import { } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { presetsDefaultModel } from '../models/preset.model'; import { ShareDataRow } from './../data/share-data-row.model'; import { ShareDataTableAdapter } from './../data/share-datatable-adapter'; @@ -70,94 +71,140 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte @ContentChild(DataColumnListComponent) columnList: DataColumnListComponent; + /** Define a set of CSS styles styles to apply depending on the permission + * of the user on that node. See the Permission Style model + * page for further details and examples. + */ @Input() permissionsStyle: PermissionStyleModel[] = []; + /** The default route for all the location-based columns (if declared). */ @Input() locationFormat: string = '/'; + /** Toggles navigation to folder content or file preview */ @Input() navigate: boolean = true; + /** User interaction for folder navigation or file preview. Valid values are "click" and "dblclick". */ @Input() navigationMode: string = DocumentListComponent.DOUBLE_CLICK_NAVIGATION; // click|dblclick + /** Show document thumbnails rather than icons */ @Input() thumbnails: boolean = false; + /** Row selection mode. Can be null, `single` or `multiple`. For `multiple` mode, + * you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. + */ @Input() selectionMode: string = 'single'; // null|single|multiple + /** Toggles multiselect mode */ @Input() multiselect: boolean = false; + /** Toggles content actions for each row */ @Input() contentActions: boolean = false; + /** Position of the content actions dropdown menu. Can be set to "left" or "right". */ @Input() contentActionsPosition: string = 'right'; // left|right + /** Toggles context menus for each row */ @Input() contextMenuActions: boolean = false; + /** Custom image for empty folder */ @Input() emptyFolderImageUrl: string = './assets/images/empty_doc_lib.svg'; + /** Toggle file drop support for rows (see Upload Directive for further details */ @Input() allowDropFiles: boolean = false; + /** Defines default sorting. The format is an array of 2 strings `[key, direction]` + * i.e. `['name', 'desc']` or `['name', 'asc']`. Set this value only if you want to + * override the default sorting detected by the component based on columns. + */ @Input() sorting: string[]; + /** The inline style to apply to every row. See + * the Angular NgStyle + * docs for more details and usage examples. + */ @Input() rowStyle: string; + /** The CSS class to apply to every row */ @Input() rowStyleClass: string; + /** Toggles the loading state and animated spinners for the component. Used in + * combination with `navigate=false` to perform custom navigation and loading + * state indication. + */ @Input() loading: boolean = false; + /** Custom row filter */ @Input() rowFilter: any | null = null; + /** Custom image resolver */ @Input() imageResolver: any | null = null; - // The identifier of a node. You can also use one of these well-known aliases: -my- | -shared- | -root- + /** The ID of the folder node to display or a reserved string alias for special sources */ @Input() currentFolderId: string = null; + /** Currently displayed folder node */ @Input() folderNode: MinimalNodeEntryEntity = null; + /** The Document list will show all the nodes contained in the NodePaging entity */ @Input() node: NodePaging = null; + /** Default value is stored into user preference settings */ @Input() maxItems: number; + /** Number of elements to skip over for pagination purposes */ @Input() skipCount: number = 0; + /** Set document list to work in infinite scrolling mode */ @Input() enableInfiniteScrolling: boolean = false; + /** Emitted when the user clicks a list node */ @Output() nodeClick: EventEmitter = new EventEmitter(); + /** Emitted when the user double-clicks a list node */ @Output() nodeDblClick: EventEmitter = new EventEmitter(); + /** Emitted when the current display folder changes */ @Output() folderChange: EventEmitter = new EventEmitter(); + /** Emitted when the user acts upon files with either single or double click + * (depends on `navigation-mode`). Useful for integration with the + * Viewer component. + */ @Output() preview: EventEmitter = new EventEmitter(); + /** Emitted when the Document List has loaded all items and is ready for use */ @Output() ready: EventEmitter = new EventEmitter(); + /** Emitted when the API fails to get the Document List data */ @Output() error: EventEmitter = new EventEmitter(); @@ -174,7 +221,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte noPermission: boolean = false; selection = new Array(); - pagination = new Subject(); + pagination: BehaviorSubject; private layoutPresets = {}; private currentNodeAllowableOperations: string[] = []; @@ -190,7 +237,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte private preferences: UserPreferencesService) { this.maxItems = this.preferences.paginationSize; - this.pagination.next( { + this.pagination = new BehaviorSubject( { maxItems: this.preferences.paginationSize, skipCount: 0, totalItems: 0, @@ -874,7 +921,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte } get supportedPageSizes(): number[] { - return this.appConfig.get('document-list.supportedPageSizes', [5, 10, 15, 25]); + return this.preferences.getDifferentPageSizes(); } ngOnDestroy() { diff --git a/lib/content-services/document-list/services/document-actions.service.spec.ts b/lib/content-services/document-list/services/document-actions.service.spec.ts index f49fb7a620..d6b83d5cf4 100644 --- a/lib/content-services/document-list/services/document-actions.service.spec.ts +++ b/lib/content-services/document-list/services/document-actions.service.spec.ts @@ -32,7 +32,7 @@ describe('DocumentActionsService', () => { beforeEach(() => { documentListService = new DocumentListServiceMock(); contentService = new ContentService(null, null, null, null); - nodeActionsService = new NodeActionsService(null, null, null); + nodeActionsService = new NodeActionsService(null, null); service = new DocumentActionsService(nodeActionsService, documentListService, contentService); }); diff --git a/lib/content-services/document-list/services/folder-actions.service.spec.ts b/lib/content-services/document-list/services/folder-actions.service.spec.ts index c32baa93d7..371d4846ab 100644 --- a/lib/content-services/document-list/services/folder-actions.service.spec.ts +++ b/lib/content-services/document-list/services/folder-actions.service.spec.ts @@ -23,6 +23,7 @@ import { ContentActionHandler } from '../models/content-action.model'; import { DocumentListService } from './document-list.service'; import { FolderActionsService } from './folder-actions.service'; import { NodeActionsService } from './node-actions.service'; +import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service'; describe('FolderActionsService', () => { @@ -37,7 +38,8 @@ describe('FolderActionsService', () => { FolderActionsService, NodeActionsService, TranslationService, - NotificationService + NotificationService, + ContentNodeDialogService ] }).compileComponents(); })); diff --git a/lib/content-services/document-list/services/node-actions.service.service.spec.ts b/lib/content-services/document-list/services/node-actions.service.service.spec.ts new file mode 100644 index 0000000000..600a94d472 --- /dev/null +++ b/lib/content-services/document-list/services/node-actions.service.service.spec.ts @@ -0,0 +1,106 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { AppConfigService } from '@alfresco/adf-core'; +import { DocumentListService } from './document-list.service'; +import { NodeActionsService } from './node-actions.service'; +import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service'; +import { Observable } from 'rxjs/Observable'; + +const fakeNode: MinimalNodeEntryEntity = { + id: 'fake' +}; + +describe('NodeActionsService', () => { + + let service: NodeActionsService; + let documentListService: DocumentListService; + let contentDialogService: ContentNodeDialogService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + NodeActionsService, + DocumentListService, + ContentNodeDialogService + ] + }).compileComponents(); + })); + + beforeEach(() => { + let appConfig: AppConfigService = TestBed.get(AppConfigService); + appConfig.config.ecmHost = 'http://localhost:9876/ecm'; + + service = TestBed.get(NodeActionsService); + documentListService = TestBed.get(DocumentListService); + contentDialogService = TestBed.get(ContentNodeDialogService); + }); + + it('should be able to create the service', () => { + expect(service).not.toBeNull(); + }); + + it('should be able to copy content', async(() => { + spyOn(documentListService, 'copyNode').and.returnValue(Observable.of('FAKE-OK')); + spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode])); + + service.copyContent(fakeNode, 'allowed').subscribe((value) => { + expect(value).toBe('OPERATION.SUCCES.CONTENT.COPY'); + }); + })); + + it('should be able to move content', async(() => { + spyOn(documentListService, 'moveNode').and.returnValue(Observable.of('FAKE-OK')); + spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode])); + + service.moveContent(fakeNode, 'allowed').subscribe((value) => { + expect(value).toBe('OPERATION.SUCCES.CONTENT.MOVE'); + }); + })); + + it('should be able to move folder', async(() => { + spyOn(documentListService, 'moveNode').and.returnValue(Observable.of('FAKE-OK')); + spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode])); + + service.moveFolder(fakeNode, 'allowed').subscribe((value) => { + expect(value).toBe('OPERATION.SUCCES.FOLDER.MOVE'); + }); + })); + + it('should be able to copy folder', async(() => { + spyOn(documentListService, 'copyNode').and.returnValue(Observable.of('FAKE-OK')); + spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode])); + + service.copyFolder(fakeNode, 'allowed').subscribe((value) => { + expect(value).toBe('OPERATION.SUCCES.FOLDER.COPY'); + }); + })); + + it('should be able to propagate the dialog error', async(() => { + spyOn(documentListService, 'copyNode').and.returnValue(Observable.throw('FAKE-KO')); + spyOn(contentDialogService, 'openCopyMoveDialog').and.returnValue(Observable.of([fakeNode])); + + service.copyFolder(fakeNode, '!allowed').subscribe((value) => { + }, (error) => { + expect(error).toBe('FAKE-KO'); + }); + })); + +}); diff --git a/lib/content-services/document-list/services/node-actions.service.ts b/lib/content-services/document-list/services/node-actions.service.ts index 42ccb6e951..fbbad226f2 100644 --- a/lib/content-services/document-list/services/node-actions.service.ts +++ b/lib/content-services/document-list/services/node-actions.service.ts @@ -15,23 +15,17 @@ * limitations under the License. */ -import { DataColumn } from '@alfresco/adf-core'; -import { ContentService } from '@alfresco/adf-core'; -import { EventEmitter, Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material'; +import { Injectable } from '@angular/core'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; import { Subject } from 'rxjs/Subject'; -import { ContentNodeSelectorComponent } from '../../content-node-selector/content-node-selector.component'; -import { ContentNodeSelectorComponentData } from '../../content-node-selector/content-node-selector.component-data.interface'; -import { ShareDataRow } from '../data/share-data-row.model'; import { DocumentListService } from './document-list.service'; +import { ContentNodeDialogService } from '../../content-node-selector/content-node-dialog.service'; @Injectable() export class NodeActionsService { - constructor(private dialog: MatDialog, - private documentListService?: DocumentListService, - private contentService?: ContentService) {} + constructor(private contentDialogService: ContentNodeDialogService, + private documentListService?: DocumentListService) {} /** * Copy content node @@ -84,51 +78,20 @@ export class NodeActionsService { private doFileOperation(action: string, type: string, contentEntry: MinimalNodeEntryEntity, permission?: string): Subject { const observable: Subject = new Subject(); - if (this.contentService.hasPermission(contentEntry, permission)) { - const data: ContentNodeSelectorComponentData = { - title: `${action} '${contentEntry.name}' to ...`, - actionName: action, - currentFolderId: contentEntry.parentId, - rowFilter: this.rowFilter.bind(this, contentEntry.id), - imageResolver: this.imageResolver.bind(this), - select: new EventEmitter() - }; - - this.dialog.open(ContentNodeSelectorComponent, { data, panelClass: 'adf-content-node-selector-dialog', width: '630px' }); - - data.select.subscribe((selections: MinimalNodeEntryEntity[]) => { + this.contentDialogService + .openCopyMoveDialog(action, contentEntry, permission) + .subscribe((selections: MinimalNodeEntryEntity[]) => { const selection = selections[0]; this.documentListService[`${action}Node`].call(this.documentListService, contentEntry.id, selection.id) .subscribe( - observable.next.bind(observable, `OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`), - observable.error.bind(observable) + observable.next.bind(observable, `OPERATION.SUCCES.${type.toUpperCase()}.${action.toUpperCase()}`), + observable.error.bind(observable) ); - this.dialog.closeAll(); + }, + (error) => { + observable.error(error); + return observable; }); - - return observable; - } else { - observable.error(new Error(JSON.stringify({ error: { statusCode: 403 } }))); - return observable; - } - } - - private rowFilter(currentNodeId, row: ShareDataRow): boolean { - const node: MinimalNodeEntryEntity = row.node.entry; - - if (node.id === currentNodeId || node.isFile) { - return false; - } else { - return true; - } - } - - private imageResolver(row: ShareDataRow, col: DataColumn): string|null { - const entry: MinimalNodeEntryEntity = row.node.entry; - if (!this.contentService.hasPermission(entry, 'create')) { - return this.documentListService.getMimeTypeIcon('disable/folder'); - } - - return null; + return observable; } } diff --git a/lib/content-services/folder-directive/folder-create.directive.ts b/lib/content-services/folder-directive/folder-create.directive.ts index 493b0b20c1..b7bcc6b241 100644 --- a/lib/content-services/folder-directive/folder-create.directive.ts +++ b/lib/content-services/folder-directive/folder-create.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, HostListener, Input } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material'; import { MinimalNodeEntryEntity } from 'alfresco-js-api'; diff --git a/lib/content-services/folder-directive/folder-edit.directive.ts b/lib/content-services/folder-directive/folder-edit.directive.ts index 3db1a46751..17e3f8b760 100644 --- a/lib/content-services/folder-directive/folder-edit.directive.ts +++ b/lib/content-services/folder-directive/folder-edit.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, ElementRef, HostListener, Input } from '@angular/core'; import { MatDialog, MatDialogConfig } from '@angular/material'; diff --git a/lib/content-services/i18n/de.json b/lib/content-services/i18n/de.json index 40e2339672..fcb846301a 100644 --- a/lib/content-services/i18n/de.json +++ b/lib/content-services/i18n/de.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Startformular" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Vorschau nicht verfügbar" - }, - "FIELD": { - "UPLOAD": "Hochladen", - "REQUIRED": "*Erforderlich", - "VALIDATOR": { - "INVALID_NUMBER": "Verwenden Sie ein anderes Zahlenformat", - "INVALID_DATE": "Verwenden Sie ein anderes Datumsformat", - "INVALID_VALUE": "Geben Sie einen anderen Wert ein", - "NOT_GREATER_THAN": "Darf nicht größer als {{ maxValue }} sein", - "NOT_LESS_THAN": "Darf nicht kleiner als {{ minValue }} sein", - "AT_LEAST_LONG": "Geben Sie mindestens {{ minLength }} Zeichen ein", - "NO_LONGER_THAN": "Geben Sie höchstens {{ maxLength }} Zeichen ein" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Dieser Ordner ist leer" @@ -62,7 +41,8 @@ "CHOOSE": "Auswählen", "COPY": "Kopieren", "MOVE": "Verschieben", - "NO_RESULTS": "Keine Ergebnisse gefunden" + "NO_RESULTS": "Keine Ergebnisse gefunden", + "SELECT_LOCATION": "Speicherort auswählen" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "Sie verfügen nicht über die nötige Berechtigung ('{{permission}}'), um {{type}} zu {{action}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Eigenschaften", + "NAME": "Name", + "TITLE": "Titel", + "DESCRIPTION": "Beschreibung", + "AUTHOR": "Autor", + "MIMETYPE": "MIME-Type", + "SIZE": "Größe", + "CREATOR": "Ersteller", + "CREATED_DATE": "Datum der Erstellung", + "MODIFIER": "Bearbeiter", + "MODIFIED_DATE": "Datum der Änderung" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/en.json b/lib/content-services/i18n/en.json index 0c242715b1..77b90fcd5a 100644 --- a/lib/content-services/i18n/en.json +++ b/lib/content-services/i18n/en.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Start Form" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Preview not available" - }, - "FIELD": { - "UPLOAD": "UPLOAD", - "REQUIRED": "*Required", - "VALIDATOR": { - "INVALID_NUMBER": "Use a different number format", - "INVALID_DATE": "Use a different date format", - "INVALID_VALUE": "Enter a different value", - "NOT_GREATER_THAN": "Can't be greater than {{ maxValue }}", - "NOT_LESS_THAN": "Can't be less than {{ minValue }}", - "AT_LEAST_LONG": "Enter at least {{ minLength }} characters", - "NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "This folder is empty" @@ -188,6 +167,7 @@ }, "METADATA": { "BASIC": { + "HEADER": "Properties", "NAME": "Name", "TITLE": "Title", "DESCRIPTION": "Description", diff --git a/lib/content-services/i18n/es.json b/lib/content-services/i18n/es.json index fc1c4cbc42..27e670e9f4 100644 --- a/lib/content-services/i18n/es.json +++ b/lib/content-services/i18n/es.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Iniciar formulario" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Vista previa no disponible" - }, - "FIELD": { - "UPLOAD": "Cargar", - "REQUIRED": "*Requerido", - "VALIDATOR": { - "INVALID_NUMBER": "Utilizar un formato de número diferente", - "INVALID_DATE": "Utilizar un formato de fecha diferente", - "INVALID_VALUE": "Introducir un valor diferente", - "NOT_GREATER_THAN": "No puede ser mayor que {{ maxValue }}", - "NOT_LESS_THAN": "No puede ser menor que {{ minValue }}", - "AT_LEAST_LONG": "Introducir al menos {{ minLength }} caracteres", - "NO_LONGER_THAN": "No introducir más de {{ maxLength }} caracteres" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Esta carpeta está vacía" @@ -62,7 +41,8 @@ "CHOOSE": "Elegir", "COPY": "Copiar", "MOVE": "Mover", - "NO_RESULTS": "Ningún resultado encontrado" + "NO_RESULTS": "Ningún resultado encontrado", + "SELECT_LOCATION": "Seleccionar ubicación" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "No tiene permiso {{permission}} para {{action}} el {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Propiedades", + "NAME": "Nombre", + "TITLE": "Título", + "DESCRIPTION": "Descripción", + "AUTHOR": "Autor", + "MIMETYPE": "Tipo MIME", + "SIZE": "Tamaño", + "CREATOR": "Creador", + "CREATED_DATE": "Fecha de creación", + "MODIFIER": "Modificador", + "MODIFIED_DATE": "Fecha de modificación" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/fr.json b/lib/content-services/i18n/fr.json index ee5b4847c0..e07899c0f7 100644 --- a/lib/content-services/i18n/fr.json +++ b/lib/content-services/i18n/fr.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Démarrer le formulaire" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Aperçu non disponible" - }, - "FIELD": { - "UPLOAD": "Importer", - "REQUIRED": "*Obligatoire", - "VALIDATOR": { - "INVALID_NUMBER": "Utiliser un format de numéro différent", - "INVALID_DATE": "Utiliser un format de date différent", - "INVALID_VALUE": "Entrer une valeur différente", - "NOT_GREATER_THAN": "Ne peut pas être supérieur à {{ maxValue }}", - "NOT_LESS_THAN": "Ne peut pas être inférieur à {{ minValue }}", - "AT_LEAST_LONG": "Entrez {{ minLength }} caractères minimum", - "NO_LONGER_THAN": "Entrez {{ maxLength }} caractères maximum" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Ce dossier est vide" @@ -62,7 +41,8 @@ "CHOOSE": "Choisir", "COPY": "Copier", "MOVE": "Déplacer", - "NO_RESULTS": "Aucun résultat trouvé" + "NO_RESULTS": "Aucun résultat trouvé", + "SELECT_LOCATION": "Sélectionner l'emplacement" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "Vous n'avez pas les droits d'accès {{permission}} pour {{action}} le {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Propriétés", + "NAME": "Nom", + "TITLE": "Titre", + "DESCRIPTION": "Description", + "AUTHOR": "Auteur", + "MIMETYPE": "Type MIME", + "SIZE": "Taille", + "CREATOR": "Créateur", + "CREATED_DATE": "Date de création", + "MODIFIER": "Modificateur", + "MODIFIED_DATE": "Date de Modification" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/it.json b/lib/content-services/i18n/it.json index ee66c94228..11d6ba5a6a 100644 --- a/lib/content-services/i18n/it.json +++ b/lib/content-services/i18n/it.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Modulo di inizio" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Anteprima non disponibile" - }, - "FIELD": { - "UPLOAD": "Carica", - "REQUIRED": "*Obbligatorio", - "VALIDATOR": { - "INVALID_NUMBER": "Usa un formato numerico diverso", - "INVALID_DATE": "Usa un formato di data diverso", - "INVALID_VALUE": "Immetti un valore diverso", - "NOT_GREATER_THAN": "Non può essere maggiore di {{ maxValue }}", - "NOT_LESS_THAN": "Non può essere inferiore di {{ minValue }}", - "AT_LEAST_LONG": "Immetti almeno {{ minLength }} caratteri", - "NO_LONGER_THAN": "Immetti non più di {{ maxLength }} caratteri" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Questa cartella è vuota" @@ -62,7 +41,8 @@ "CHOOSE": "Scegli", "COPY": "Copia", "MOVE": "Sposta", - "NO_RESULTS": "Nessun risultato trovato" + "NO_RESULTS": "Nessun risultato trovato", + "SELECT_LOCATION": "Seleziona località" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "Non hai l'autorizzazione {{permission}} per {{action}} {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Proprietà", + "NAME": "Nome", + "TITLE": "Titolo", + "DESCRIPTION": "Descrizione", + "AUTHOR": "Autore", + "MIMETYPE": "Tipo MIME", + "SIZE": "Dimensione", + "CREATOR": "Creatore", + "CREATED_DATE": "Data di creazione", + "MODIFIER": "Modificatore", + "MODIFIED_DATE": "Data di modifica" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/ja.json b/lib/content-services/i18n/ja.json index e5d050f084..23cf0e7554 100644 --- a/lib/content-services/i18n/ja.json +++ b/lib/content-services/i18n/ja.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "開始フォーム" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "プレビューは使用できません" - }, - "FIELD": { - "UPLOAD": "アップロード", - "REQUIRED": "*必須", - "VALIDATOR": { - "INVALID_NUMBER": "別の番号形式を使用してください", - "INVALID_DATE": "別の日付形式を使用してください", - "INVALID_VALUE": "別の値を入力してください", - "NOT_GREATER_THAN": "{{ maxValue }} より大きい値にすることはできません", - "NOT_LESS_THAN": "{{ minValue }} より小さい値にすることはできません", - "AT_LEAST_LONG": "{{ minLength }} 文字以上で入力してください", - "NO_LONGER_THAN": "{{ maxLength }} 文字以内で入力してください" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "このフォルダは空です" @@ -62,7 +41,8 @@ "CHOOSE": "選択", "COPY": "コピー", "MOVE": "移動", - "NO_RESULTS": "一致するアイテムはありません" + "NO_RESULTS": "一致するアイテムはありません", + "SELECT_LOCATION": "場所の選択" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "{{type}} を {{action}} するための {{permission}} 権限がありません" + }, + "METADATA": { + "BASIC": { + "HEADER": "プロパティ", + "NAME": "名前", + "TITLE": "タイトル", + "DESCRIPTION": "説明", + "AUTHOR": "作成者", + "MIMETYPE": "MIME タイプ", + "SIZE": "サイズ", + "CREATOR": "作成者", + "CREATED_DATE": "作成日", + "MODIFIER": "変更者", + "MODIFIED_DATE": "変更日" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/nb.json b/lib/content-services/i18n/nb.json index 040fad2225..506a3add96 100644 --- a/lib/content-services/i18n/nb.json +++ b/lib/content-services/i18n/nb.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Start skjema" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Forhåndsvisning ikke tilgjengelig" - }, - "FIELD": { - "UPLOAD": "Laster opp", - "REQUIRED": "*Påkrevd", - "VALIDATOR": { - "INVALID_NUMBER": "Bruk et annet nummerformat", - "INVALID_DATE": "Bruk et annet datoformat", - "INVALID_VALUE": "Angi en annen verdi", - "NOT_GREATER_THAN": "Kan ikke være større enn {{ maxValue }}", - "NOT_LESS_THAN": "Kan ikke være mindre enn {{ minValue }}", - "AT_LEAST_LONG": "Angi minst {{ minLength }} tegn", - "NO_LONGER_THAN": "Ikke angi flere enn {{ maxLength }} tegn" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Denne mappen er tom" @@ -62,7 +41,8 @@ "CHOOSE": "Velg", "COPY": "Kopier", "MOVE": "Flytt", - "NO_RESULTS": "Ingen resultater funnet" + "NO_RESULTS": "Ingen resultater funnet", + "SELECT_LOCATION": "Velg plassering" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "Du har ikke {{permission}} tillatelsen til å {{action}} {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Egenskaper", + "NAME": "Navn", + "TITLE": "Tittel", + "DESCRIPTION": "Beskrivelse", + "AUTHOR": "Forfatter", + "MIMETYPE": "Mimetype", + "SIZE": "Størrelse", + "CREATOR": "Oppretter", + "CREATED_DATE": "Opprettelsesdato", + "MODIFIER": "Modifikator", + "MODIFIED_DATE": "Endringsdato" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/nl.json b/lib/content-services/i18n/nl.json index ab84be4e38..d0106b71f1 100644 --- a/lib/content-services/i18n/nl.json +++ b/lib/content-services/i18n/nl.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Beginformulier" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Preview niet beschikbaar" - }, - "FIELD": { - "UPLOAD": "Uploaden", - "REQUIRED": "*Verplicht", - "VALIDATOR": { - "INVALID_NUMBER": "Gebruik een andere getalnotatie", - "INVALID_DATE": "Gebruik een andere datumnotatie", - "INVALID_VALUE": "Voer een andere waarde in", - "NOT_GREATER_THAN": "Mag niet groter zijn dan {{ maxValue }}", - "NOT_LESS_THAN": "Mag niet kleiner zijn dan {{ minValue }}", - "AT_LEAST_LONG": "Voer minimaal {{ minLength }} tekens in", - "NO_LONGER_THAN": "Voer maximaal {{ maxLength }} tekens in" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Deze map is leeg" @@ -62,7 +41,8 @@ "CHOOSE": "Kiezen", "COPY": "Kopiëren", "MOVE": "Verplaatsen", - "NO_RESULTS": "Geen resultaten gevonden" + "NO_RESULTS": "Geen resultaten gevonden", + "SELECT_LOCATION": "Locatie selecteren" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "U hebt geen {{permission}} rechten voor het {{action}} van {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Eigenschappen", + "NAME": "Naam", + "TITLE": "Titel", + "DESCRIPTION": "Beschrijving", + "AUTHOR": "Auteur", + "MIMETYPE": "Mimetype", + "SIZE": "Grootte", + "CREATOR": "Maker", + "CREATED_DATE": "Datum gemaakt", + "MODIFIER": "Gewijzigd door", + "MODIFIED_DATE": "Datum gewijzigd" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/pt-BR.json b/lib/content-services/i18n/pt-BR.json index 7f0a53083c..12fbe14993 100644 --- a/lib/content-services/i18n/pt-BR.json +++ b/lib/content-services/i18n/pt-BR.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Iniciar formulário" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Pré-visualização não disponível" - }, - "FIELD": { - "UPLOAD": "Carregar", - "REQUIRED": "*Obrigatório", - "VALIDATOR": { - "INVALID_NUMBER": "Use um formato de número diferente", - "INVALID_DATE": "Use um formato de data diferente", - "INVALID_VALUE": "Insira um valor diferente", - "NOT_GREATER_THAN": "Não pode ser maior do que {{ maxValue }}", - "NOT_LESS_THAN": "Não pode ser menor do que {{ minValue }}", - "AT_LEAST_LONG": "Insira no mínimo {{ minLength }} caracteres", - "NO_LONGER_THAN": "Insira no máximo {{ maxLength }} caracteres" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Esta pasta está vazia" @@ -62,7 +41,8 @@ "CHOOSE": "Escolher", "COPY": "Copiar", "MOVE": "Mover", - "NO_RESULTS": "Nenhum resultado encontrado" + "NO_RESULTS": "Nenhum resultado encontrado", + "SELECT_LOCATION": "Selecionar localização" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "Você não tem a permissão {{permission}} para {{action}} o {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Propriedades", + "NAME": "Nome", + "TITLE": "Título", + "DESCRIPTION": "Descrição", + "AUTHOR": "Autor", + "MIMETYPE": "Tipo mime", + "SIZE": "Tamanho", + "CREATOR": "Criador", + "CREATED_DATE": "Data de criação", + "MODIFIER": "Modificador", + "MODIFIED_DATE": "Data de modificação" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/ru.json b/lib/content-services/i18n/ru.json index a78ca9cba3..82df9a88b5 100644 --- a/lib/content-services/i18n/ru.json +++ b/lib/content-services/i18n/ru.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "Запустить форму" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "Предварительный просмотр недоступен" - }, - "FIELD": { - "UPLOAD": "Загрузить", - "REQUIRED": "*Обязательно для заполнения", - "VALIDATOR": { - "INVALID_NUMBER": "Используйте другой формат числа", - "INVALID_DATE": "Используйте другой формат даты", - "INVALID_VALUE": "Введите другое значение", - "NOT_GREATER_THAN": "Не может быть более {{ maxValue }}", - "NOT_LESS_THAN": "Не может быть менее {{ minValue }}", - "AT_LEAST_LONG": "Введите не менее {{ minLength }} символ", - "NO_LONGER_THAN": "Введите не более {{ maxLength }} символ" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "Эта папка пуста" @@ -62,7 +41,8 @@ "CHOOSE": "Выбрать", "COPY": "Копировать", "MOVE": "Переместить", - "NO_RESULTS": "Результаты не найдены" + "NO_RESULTS": "Результаты не найдены", + "SELECT_LOCATION": "Выбрать местоположение" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "У вас нет разрешения {{permission}}, чтобы {{action}} {{type}}" + }, + "METADATA": { + "BASIC": { + "HEADER": "Свойства", + "NAME": "Имя", + "TITLE": "Заголовок", + "DESCRIPTION": "Описание", + "AUTHOR": "Автор", + "MIMETYPE": "Тип MIME", + "SIZE": "Размер", + "CREATOR": "Создатель", + "CREATED_DATE": "Дата создания", + "MODIFIER": "Редактор", + "MODIFIED_DATE": "Дата изменения" + } } } \ No newline at end of file diff --git a/lib/content-services/i18n/zh-CN.json b/lib/content-services/i18n/zh-CN.json index af51418d22..542760ac2f 100644 --- a/lib/content-services/i18n/zh-CN.json +++ b/lib/content-services/i18n/zh-CN.json @@ -1,25 +1,4 @@ { - "FORM": { - "START_FORM": { - "TITLE": "启动表单" - }, - "PREVIEW": { - "IMAGE_NOT_AVAILABLE": "预览不可用" - }, - "FIELD": { - "UPLOAD": "上传", - "REQUIRED": "*必填项", - "VALIDATOR": { - "INVALID_NUMBER": "使用其他数字格式", - "INVALID_DATE": "使用其他日期格式", - "INVALID_VALUE": "输入其他值", - "NOT_GREATER_THAN": "不得大于 {{ maxValue }}", - "NOT_LESS_THAN": "不得小于 {{ minValue }}", - "AT_LEAST_LONG": "输入至少 {{ minLength }} 个字符", - "NO_LONGER_THAN": "不得输入超过 {{ maxLength }} 个字符" - } - } - }, "ADF-DOCUMENT-LIST": { "EMPTY": { "HEADER": "此文件夹为空" @@ -62,7 +41,8 @@ "CHOOSE": "选择", "COPY": "复制", "MOVE": "移动", - "NO_RESULTS": "未找到结果" + "NO_RESULTS": "未找到结果", + "SELECT_LOCATION": "选择位置" }, "OPERATION": { "SUCCES": { @@ -184,5 +164,20 @@ }, "PERMISSON": { "LACKOF": "您没有 {{action}} {{type}} 的 {{permission}} 权限" + }, + "METADATA": { + "BASIC": { + "HEADER": "属性", + "NAME": "名称", + "TITLE": "标题", + "DESCRIPTION": "说明", + "AUTHOR": "作者", + "MIMETYPE": "MIME 类型", + "SIZE": "字号(&S)", + "CREATOR": "创建者", + "CREATED_DATE": "创建日期", + "MODIFIER": "修改者", + "MODIFIED_DATE": "修改日期" + } } } \ No newline at end of file diff --git a/lib/content-services/material.module.ts b/lib/content-services/material.module.ts index 9c79ff8008..4eb6c5c1f4 100644 --- a/lib/content-services/material.module.ts +++ b/lib/content-services/material.module.ts @@ -29,6 +29,7 @@ import { MatProgressBarModule, MatProgressSpinnerModule, MatRippleModule, + MatExpansionModule, MatSelectModule } from '@angular/material'; @@ -46,6 +47,7 @@ export function modules() { MatRippleModule, MatMenuModule, MatOptionModule, + MatExpansionModule, MatSelectModule ]; } diff --git a/lib/content-services/package.json b/lib/content-services/package.json index ade9d2981b..e7b887f6ad 100644 --- a/lib/content-services/package.json +++ b/lib/content-services/package.json @@ -1,7 +1,7 @@ { "name": "@alfresco/adf-content-services", "description": "Alfresco ADF content services", - "version": "2.0.0", + "version": "2.1.0", "author": "Alfresco Software, Ltd.", "main": "bundles/adf-content-services.js", "repository": { @@ -12,22 +12,22 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "@angular/animations": "5.0.0", - "@angular/cdk": "5.0.0-rc0", - "@angular/common": "5.0.0", - "@angular/compiler": "5.0.0", - "@angular/core": "5.0.0", - "@angular/flex-layout": "2.0.0-beta.10", - "@angular/forms": "5.0.0", - "@angular/http": "5.0.0", - "@angular/material": "5.0.0-rc0", - "@angular/material-moment-adapter": "^5.0.0-rc0", - "@angular/platform-browser": "5.0.0", - "@angular/platform-browser-dynamic": "5.0.0", - "@angular/router": "5.0.0", + "@angular/animations": "5.1.1", + "@angular/cdk": "5.0.1", + "@angular/common": "5.1.1", + "@angular/compiler": "5.1.1", + "@angular/core": "5.1.1", + "@angular/flex-layout": "2.0.0-beta.12", + "@angular/forms": "5.1.1", + "@angular/http": "5.1.1", + "@angular/material": "5.0.1", + "@angular/material-moment-adapter": "5.0.1", + "@angular/platform-browser": "5.1.1", + "@angular/platform-browser-dynamic": "5.1.1", + "@angular/router": "5.1.1", "@ngx-translate/core": "8.0.0", - "alfresco-js-api": "2.0.0", - "@alfresco/adf-core": "2.0.0", + "alfresco-js-api": "2.1.0", + "@alfresco/adf-core": "2.1.0", "chart.js": "2.5.0", "core-js": "2.4.1", "hammerjs": "2.0.8", diff --git a/lib/content-services/search/components/search-control.component.html b/lib/content-services/search/components/search-control.component.html index 28af9a18b7..3d33f1ac6c 100644 --- a/lib/content-services/search/components/search-control.component.html +++ b/lib/content-services/search/components/search-control.component.html @@ -27,13 +27,14 @@ + [maxResults]="liveSearchMaxResults" + [queryBody]="customQueryBody"> { - if (searchNode.query.query === 'FAKE_SEARCH_EXMPL') { + if (searchNode && searchNode.query.query === 'FAKE_SEARCH_EXMPL') { return Observable.of(differentResult); } - if (searchNode.filterQueries.length === 1 && + if (searchNode && searchNode.filterQueries.length === 1 && searchNode.filterQueries[0].query === "TYPE:'cm:folder'") { return Observable.of(folderResult); } @@ -132,18 +132,18 @@ describe('SearchComponent', () => { }); it('should perform a search based on the query node given', async(() => { - spyOn(searchService, 'search') - .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); + spyOn(searchService, 'searchByQueryBody') + .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); let fakeSearchNode: QueryBody = { query: { - query: '' + query: 'TEST-FAKE-NODE' }, filterQueries: [ { 'query': "TYPE:'cm:folder'" } ] }; - component.setSearchNodeTo(fakeSearchNode); component.setSearchWordTo('searchTerm'); + component.setSearchNodeTo(fakeSearchNode); fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); @@ -156,7 +156,7 @@ describe('SearchComponent', () => { it('should perform a search with a defaultNode if no searchnode is given', async(() => { spyOn(searchService, 'search') - .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); + .and.returnValue(Observable.of(result)); component.setSearchWordTo('searchTerm'); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -169,7 +169,7 @@ describe('SearchComponent', () => { })); it('should perform a search with the searchNode given', async(() => { - spyOn(searchService, 'search') + spyOn(searchService, 'searchByQueryBody') .and.callFake((searchObj) => fakeNodeResultSearch(searchObj)); let fakeSearchNode: QueryBody = { query: { @@ -179,6 +179,7 @@ describe('SearchComponent', () => { { 'query': "TYPE:'cm:folder'" } ] }; + component.setSearchWordTo('searchTerm'); component.setSearchNodeTo(fakeSearchNode); fixture.detectChanges(); fixture.whenStable().then(() => { diff --git a/lib/content-services/search/components/search.component.ts b/lib/content-services/search/components/search.component.ts index 63d8ec2090..48fc102e23 100644 --- a/lib/content-services/search/components/search.component.ts +++ b/lib/content-services/search/components/search.component.ts @@ -18,8 +18,6 @@ import { SearchService } from '@alfresco/adf-core'; import { AfterContentInit, - ChangeDetectionStrategy, - ChangeDetectorRef, Component, ContentChild, ElementRef, @@ -40,7 +38,6 @@ import { Subject } from 'rxjs/Subject'; styleUrls: ['./search.component.scss'], encapsulation: ViewEncapsulation.None, preserveWhitespaces: false, - changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'searchAutocomplete', host: { 'class': 'adf-search' @@ -63,12 +60,13 @@ export class SearchComponent implements AfterContentInit, OnChanges { @Input() skipResults: number = 0; - @Input() - searchTerm: string = ''; - + /** @deprecated in 2.1.0 */ @Input() queryBody: QueryBody; + @Input() + searchTerm: string = ''; + @Input('class') set classList(classList: string) { if (classList && classList.length) { @@ -101,13 +99,13 @@ export class SearchComponent implements AfterContentInit, OnChanges { _classList: { [key: string]: boolean } = {}; constructor(private searchService: SearchService, - private changeDetectorRef: ChangeDetectorRef, private _elementRef: ElementRef) { this.keyPressedStream.asObservable() .debounceTime(200) .subscribe((searchedWord: string) => { this.loadSearchResults(searchedWord); }); + } ngAfterContentInit() { @@ -115,14 +113,12 @@ export class SearchComponent implements AfterContentInit, OnChanges { } ngOnChanges(changes) { - this.resetResults(); - + if (changes.queryBody && + this.hasDifferentQueryBody(changes.queryBody.previousValue, changes.queryBody.currentValue)) { + this.loadSearchResults(); + } if (changes.searchTerm && changes.searchTerm.currentValue) { this.loadSearchResults(changes.searchTerm.currentValue); - } else if (changes.queryBody && changes.queryBody.currentValue) { - this.loadSearchResults(); - } else { - this.loadSearchResults(this.searchTerm); } } @@ -135,23 +131,27 @@ export class SearchComponent implements AfterContentInit, OnChanges { this.loadSearchResults(this.searchTerm); } + private hasDifferentQueryBody(previousQueryBody: QueryBody, currentQueryBody: QueryBody) { + return JSON.stringify(previousQueryBody) !== JSON.stringify(currentQueryBody); + } + private cleanResults() { if (this.results) { this.results = {}; } } - private hasValidSearchQuery(searchOpts: QueryBody) { - return searchOpts && searchOpts.query && searchOpts.query.query; - } - private loadSearchResults(searchTerm?: string) { - let searchOpts: QueryBody = this.getQueryBody(searchTerm); - - if (this.hasValidSearchQuery(searchOpts)) { - this.searchService - .search(searchOpts) - .subscribe( + this.resetResults(); + if (searchTerm) { + let search$; + if (this.queryBody) { + search$ = this.searchService.searchByQueryBody(this.queryBody); + } else { + search$ = this.searchService + .search(searchTerm, this.maxResults.toString(), this.skipResults.toString()); + } + search$.subscribe( results => { this.results = results; this.resultLoaded.emit(this.results); @@ -169,41 +169,11 @@ export class SearchComponent implements AfterContentInit, OnChanges { } } - private getQueryBody(searchTerm: string): QueryBody { - if (this.queryBody) { - if (!this.queryBody.query.query && searchTerm) { - this.queryBody.query.query = searchTerm; - } - return this.queryBody; - } else { - return this.generateDefaultSearchNode(searchTerm); - } - } - - private generateDefaultSearchNode(searchTerm: string): QueryBody { - let defaultQueryBody: QueryBody = { - query: { - query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm - }, - include: ['path', 'allowableOperations'], - paging: { - maxItems: this.maxResults.toString(), - skipCount: this.skipResults.toString() - }, - filterQueries: [ - { query: "TYPE:'cm:folder' OR TYPE:'cm:content'" }, - { query: 'NOT cm:creator:System' }] - }; - - return defaultQueryBody; - } - hidePanel() { if (this.isOpen) { this._classList['adf-search-show'] = false; this._classList['adf-search-hide'] = true; this.isOpen = false; - this.changeDetectorRef.markForCheck(); } } @@ -211,6 +181,5 @@ export class SearchComponent implements AfterContentInit, OnChanges { this.showPanel = !!this.results && !!this.results.list; this._classList['adf-search-show'] = this.showPanel; this._classList['adf-search-hide'] = !this.showPanel; - this.changeDetectorRef.markForCheck(); } } diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.html b/lib/content-services/site-dropdown/sites-dropdown.component.html index 941335de2c..0663175877 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.html +++ b/lib/content-services/site-dropdown/sites-dropdown.component.html @@ -9,8 +9,8 @@ [(ngModel)]="siteSelected" (ngModelChange)="selectedSite()"> {{'DROPDOWN.MY_FILES_OPTION' | translate}} - - {{ site.title | translate }} + + {{ site.entry.title | translate}} diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts b/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts index a5cad3b3da..821f0fa419 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts +++ b/lib/content-services/site-dropdown/sites-dropdown.component.spec.ts @@ -153,7 +153,7 @@ describe('DropdownSitesComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); - expect(fixture.nativeElement.innerText.trim()).toBe('DROPDOWN.PLACEHOLDER_LABEL'); + expect(fixture.nativeElement.innerText.trim()).toContain('DROPDOWN.PLACEHOLDER_LABEL'); }); })); @@ -166,12 +166,30 @@ describe('DropdownSitesComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); - expect(fixture.nativeElement.innerText.trim()).toBe('NODE_SELECTOR.SELECT_LOCATION'); + expect(fixture.nativeElement.innerText.trim()).toContain('NODE_SELECTOR.SELECT_LOCATION'); }); })); it('should load custom sites when the \'siteList\' input property is given a value', async(() => { - component.siteList = [{title: 'PERSONAL_FILES', guid: '-my-'}, {title: 'FILE_LIBRARIES', guid: '-mysites-'}]; + component.siteList = { + 'list': { + 'entries': [ + { + 'entry': { + 'guid': '-my-', + 'title': 'PERSONAL_FILES' + } + }, + { + 'entry': { + 'guid': '-mysites-', + 'title': 'FILE_LIBRARIES' + } + } + ] + } + }; + fixture.detectChanges(); openSelectbox(); @@ -236,7 +254,7 @@ describe('DropdownSitesComponent', () => { }); component.change.subscribe((site) => { - expect(site.guid).toBe('fake-1'); + expect(site.entry.guid).toBe('fake-1'); done(); }); }); diff --git a/lib/content-services/site-dropdown/sites-dropdown.component.ts b/lib/content-services/site-dropdown/sites-dropdown.component.ts index cf86745d9c..0949b44912 100644 --- a/lib/content-services/site-dropdown/sites-dropdown.component.ts +++ b/lib/content-services/site-dropdown/sites-dropdown.component.ts @@ -15,7 +15,8 @@ * limitations under the License. */ -import { SiteModel, SitesService } from '@alfresco/adf-core'; +import { SitesService } from '@alfresco/adf-core'; +import { SitePaging, SiteEntry } from 'alfresco-js-api'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; @Component({ @@ -29,13 +30,13 @@ export class DropdownSitesComponent implements OnInit { hideMyFiles: boolean = false; @Input() - siteList: any[] = null; + siteList: SitePaging = null; @Input() placeholder: string = 'DROPDOWN.PLACEHOLDER_LABEL'; @Output() - change: EventEmitter = new EventEmitter(); + change: EventEmitter = new EventEmitter(); public MY_FILES_VALUE = 'default'; @@ -52,15 +53,14 @@ export class DropdownSitesComponent implements OnInit { selectedSite() { let siteFound; if (this.siteSelected === this.MY_FILES_VALUE) { - siteFound = new SiteModel(); + siteFound = { entry: {}}; }else { - siteFound = this.siteList.find( site => site.guid === this.siteSelected); + siteFound = this.siteList.list.entries.find( site => site.entry.guid === this.siteSelected); } this.change.emit(siteFound); } setDefaultSiteList() { - this.siteList = []; this.sitesService.getSites().subscribe((result) => { this.siteList = result; }, diff --git a/lib/content-services/styles/_index.scss b/lib/content-services/styles/_index.scss index 5bae304533..550f533880 100644 --- a/lib/content-services/styles/_index.scss +++ b/lib/content-services/styles/_index.scss @@ -1,6 +1,7 @@ @import '../breadcrumb/breadcrumb.component'; @import '../breadcrumb/dropdown-breadcrumb.component'; -@import '../content-node-selector/content-node-selector.component'; +@import '../content-node-selector/content-node-selector-panel.component'; +@import '../content-node-selector/name-location-cell/name-location-cell.component'; @import '../document-list/components/document-list.component'; @import '../upload/components/file-uploading-list-row.component'; @@ -11,19 +12,20 @@ @import '../dialogs/folder.dialog'; -@import '../content-metadata/content-metadata.component'; -@import '../content-metadata/content-metadata-card.component'; +@import '../content-node-selector/content-node-selector.component'; +@import '../content-metadata/content-metadata.module'; @mixin adf-content-services-theme($theme) { @include adf-breadcrumb-theme($theme); @include adf-breadcrumb-dropdown-theme($theme); @include adf-content-node-selector-theme($theme) ; + @include adf-name-location-cell-theme($theme); @include adf-document-list-theme($theme) ; @include adf-file-uploading-row-theme($theme); @include adf-upload-dialog-theme($theme); @include adf-search-control-theme($theme); @include adf-search-autocomplete-theme($theme); @include adf-dialog-theme($theme); - @include adf-content-metadata-theme($theme); - @include adf-content-metadata-card-theme($theme); + @include adf-content-node-selector-dialog-theme($theme) ; + @include adf-content-metadata-module-theme($theme); } diff --git a/lib/content-services/upload/components/upload-button.component.spec.ts b/lib/content-services/upload/components/upload-button.component.spec.ts index d6f2b1b2c8..031631e819 100644 --- a/lib/content-services/upload/components/upload-button.component.spec.ts +++ b/lib/content-services/upload/components/upload-button.component.spec.ts @@ -216,6 +216,17 @@ describe('UploadButtonComponent', () => { expect(addToQueueSpy.calls.mostRecent()).toBeUndefined(); }); + it('should allow file of 0 size when the max file size is set to 0', () => { + const zeroFiles: File[] = [ + { name: 'zeroFile.png', size: 0 } + ]; + component.maxFilesSize = 0; + + component.uploadFiles(zeroFiles); + + expect(addToQueueSpy.calls.mostRecent()).toBeDefined(); + }); + it('should filter out all files if maxFilesSize is <0', () => { component.maxFilesSize = -2; diff --git a/lib/content-services/upload/components/upload-button.component.ts b/lib/content-services/upload/components/upload-button.component.ts index 59151e5dac..acc7fd38e2 100644 --- a/lib/content-services/upload/components/upload-button.component.ts +++ b/lib/content-services/upload/components/upload-button.component.ts @@ -201,7 +201,7 @@ export class UploadButtonComponent implements OnInit, OnChanges, NodePermissionS private isFileSizeAcceptable(file: FileModel): boolean { let acceptableSize = true; - if ((this.maxFilesSize !== undefined && this.maxFilesSize !== null ) && (this.maxFilesSize <= 0 || file.size > this.maxFilesSize)) { + if (this.isFileSizeAllowed(file)) { acceptableSize = false; this.translateService.get('FILE_UPLOAD.MESSAGES.EXCEED_MAX_FILE_SIZE', {fileName: file.name}).subscribe((message: string) => { @@ -212,6 +212,18 @@ export class UploadButtonComponent implements OnInit, OnChanges, NodePermissionS return acceptableSize; } + private isFileSizeAllowed(file: FileModel) { + return this.isMaxFileSizeDefined() && this.isFileSizeCorrect(file); + } + + private isMaxFileSizeDefined() { + return this.maxFilesSize !== undefined && this.maxFilesSize !== null; + } + + private isFileSizeCorrect(file: FileModel) { + return this.maxFilesSize < 0 || file.size > this.maxFilesSize; + } + checkPermission() { if (this.rootFolderId) { this.getFolderNode(this.rootFolderId).subscribe( diff --git a/lib/content-services/upload/directives/file-draggable.directive.ts b/lib/content-services/upload/directives/file-draggable.directive.ts index c0cf40d4fd..e29e26e7d3 100644 --- a/lib/content-services/upload/directives/file-draggable.directive.ts +++ b/lib/content-services/upload/directives/file-draggable.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { FileUtils } from '@alfresco/adf-core'; import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; diff --git a/lib/content-services/webscript/webscript.component.spec.ts b/lib/content-services/webscript/webscript.component.spec.ts index 54e7e3b5e1..b765fc60a0 100644 --- a/lib/content-services/webscript/webscript.component.spec.ts +++ b/lib/content-services/webscript/webscript.component.spec.ts @@ -37,23 +37,21 @@ describe('WebscriptComponent', () => { declarations: [ WebscriptComponent ] - }).compileComponents(); + }).compileComponents().then(() => { + let appConfig: AppConfigService = TestBed.get(AppConfigService); + appConfig.config.ecmHost = 'http://localhost:9876/ecm'; + + fixture = TestBed.createComponent(WebscriptComponent); + component = fixture.componentInstance; + + element = fixture.nativeElement; + component = fixture.componentInstance; + component.scriptPath = 'fakePath'; + component.showData = true; + fixture.detectChanges(); + }); })); - beforeEach(() => { - let appConfig: AppConfigService = TestBed.get(AppConfigService); - appConfig.config.ecmHost = 'http://localhost:9876/ecm'; - - fixture = TestBed.createComponent(WebscriptComponent); - component = fixture.componentInstance; - - element = fixture.nativeElement; - component = fixture.componentInstance; - component.scriptPath = 'fakePath'; - component.showData = true; - fixture.detectChanges(); - }); - describe('View', () => { it('html wrapper should be present', () => { expect(element.querySelector('#webscript-html-wrapper')).toBeDefined(); diff --git a/lib/core/app-config/app-config.module.ts b/lib/core/app-config/app-config.module.ts index 8c4f579a2c..1f46cc71d8 100644 --- a/lib/core/app-config/app-config.module.ts +++ b/lib/core/app-config/app-config.module.ts @@ -18,10 +18,7 @@ import { HttpClientModule } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { AppConfigService } from './app-config.service'; - -export function startupServiceFactory(configService: AppConfigService): Function { - return () => configService.load(); -} +import { startupServiceFactory } from './startup-service-factory'; @NgModule({ imports: [ diff --git a/lib/core/app-config/app-config.service.ts b/lib/core/app-config/app-config.service.ts index b6f3054b63..d250a25379 100644 --- a/lib/core/app-config/app-config.service.ts +++ b/lib/core/app-config/app-config.service.ts @@ -30,7 +30,8 @@ export class AppConfigService { }, ecmHost: 'http://{hostname}{:port}/ecm', bpmHost: 'http://{hostname}{:port}/bpm', - logLevel: 'silent' + logLevel: 'silent', + alfrescoRepositoryName : 'alfresco-1' }; constructor(private http: HttpClient) { diff --git a/lib/core/app-config/startup-service-factory.ts b/lib/core/app-config/startup-service-factory.ts new file mode 100644 index 0000000000..8bc35093ae --- /dev/null +++ b/lib/core/app-config/startup-service-factory.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { AppConfigService } from './app-config.service'; + +export function startupServiceFactory(configService: AppConfigService): Function { + return () => configService.load(); +} diff --git a/lib/core/card-view/card-view-dateitem.component.html b/lib/core/card-view/card-view-dateitem.component.html deleted file mode 100644 index 452153ba32..0000000000 --- a/lib/core/card-view/card-view-dateitem.component.html +++ /dev/null @@ -1,30 +0,0 @@ -
{{ property.label | translate }}
-
- - - {{ property.displayValue }} - - - - - - {{ property.displayValue }} - - - - - - - - {{ property.default | translate }} - -
diff --git a/lib/core/card-view/card-view-dateitem.component.scss b/lib/core/card-view/card-view-dateitem.component.scss deleted file mode 100644 index 59b2c249bf..0000000000 --- a/lib/core/card-view/card-view-dateitem.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -@mixin adf-card-view-dateitem-theme($theme) { - - .adf { - &-invisible-date-input { - height: 24px; - width: 0; - overflow: hidden; - opacity: 0; - border: none; - margin: 0; - padding: 0; - display: none; - } - - &-dateitem-editable { - cursor: pointer; - - button.mat-icon-button { - line-height: 20px; - height: 20px; - width: 20px; - } - - mat-icon { - width: 16px; - height: 16px; - opacity: 0.5; - margin-left: 4px; - } - - &:hover mat-icon { - opacity: 1; - } - } - } -} diff --git a/lib/core/card-view/card-view-textitem.component.html b/lib/core/card-view/card-view-textitem.component.html deleted file mode 100644 index c8fcf87199..0000000000 --- a/lib/core/card-view/card-view-textitem.component.html +++ /dev/null @@ -1,51 +0,0 @@ -
{{ property.label | translate }}
-
- - - {{ property.displayValue }} - - - - {{ property.displayValue }} - - - - -
- - {{ property.displayValue }} - - create -
-
- - - - - done - clear -
-
- - {{ property.default | translate }} - -
diff --git a/lib/core/card-view/card-view-textitem.component.spec.ts b/lib/core/card-view/card-view-textitem.component.spec.ts deleted file mode 100644 index ff73e6fdbc..0000000000 --- a/lib/core/card-view/card-view-textitem.component.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -/*! - * @license - * Copyright 2016 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ - -import { HttpClientModule } from '@angular/common/http'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule } from '@angular/forms'; -import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { CardViewTextItemModel } from '../models/card-view-textitem.model'; -import { AppConfigService } from '../app-config/app-config.service'; -import { CardViewUpdateService } from '../services/card-view-update.service'; -import { LogService } from '../services/log.service'; -import { TranslateLoaderService } from '../services/translate-loader.service'; - -import { CardViewTextItemComponent } from './card-view-textitem.component'; - -describe('CardViewTextItemComponent', () => { - - let fixture: ComponentFixture; - let component: CardViewTextItemComponent; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientModule, - FormsModule, - NoopAnimationsModule, - MatDatepickerModule, - MatIconModule, - MatInputModule, - MatNativeDateModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderService - } - }) - ], - declarations: [ - CardViewTextItemComponent - ], - providers: [ - AppConfigService, - CardViewUpdateService, - LogService - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CardViewTextItemComponent); - component = fixture.componentInstance; - component.property = new CardViewTextItemModel ({ - label: 'Text label', - value: 'Lorem ipsum', - key: 'textkey', - default: 'FAKE-DEFAULT-KEY', - editable: false - }); - }); - - afterEach(() => { - fixture.destroy(); - TestBed.resetTestingModule(); - }); - - it('should render the label and value', () => { - fixture.detectChanges(); - - let labelValue = fixture.debugElement.query(By.css('.adf-property-label')); - expect(labelValue).not.toBeNull(); - expect(labelValue.nativeElement.innerText).toBe('Text label'); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum'); - }); - - it('should render the default as value if the value is empty and editable false', () => { - component.property = new CardViewTextItemModel ({ - label: 'Text label', - value: '', - key: 'textkey', - default: 'FAKE-DEFAULT-KEY', - editable: false - }); - fixture.detectChanges(); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); - }); - - it('should render the default as value if the value is empty and editable true', () => { - component.property = new CardViewTextItemModel ({ - label: 'Text label', - value: '', - key: 'textkey', - default: 'FAKE-DEFAULT-KEY', - editable: true - }); - fixture.detectChanges(); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); - }); - - it('should render the default as value if the value is empty and clickable false', () => { - component.property = new CardViewTextItemModel ({ - label: 'Text label', - value: '', - key: 'textkey', - default: 'FAKE-DEFAULT-KEY', - clickable: false - }); - fixture.detectChanges(); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); - }); - - it('should render the default as value if the value is empty and clickable true', () => { - component.property = new CardViewTextItemModel ({ - label: 'Text label', - value: '', - key: 'textkey', - default: 'FAKE-DEFAULT-KEY', - clickable: true - }); - fixture.detectChanges(); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); - }); - - it('should render value when editable:true', () => { - component.editable = true; - component.property.editable = true; - fixture.detectChanges(); - - let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); - expect(value).not.toBeNull(); - expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum'); - }); - - it('should render the edit icon in case of editable:true', () => { - component.editable = true; - component.property.editable = true; - fixture.detectChanges(); - - let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); - expect(editIcon).not.toBeNull('Edit icon should be shown'); - }); - - it('should NOT render the edit icon in case of editable:false', () => { - component.editable = false; - fixture.detectChanges(); - - let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); - expect(editIcon).toBeNull('Edit icon should NOT be shown'); - }); - - it('should NOT render the picker and toggle in case of editable:true but (general) editable:false', () => { - component.editable = false; - component.property.editable = true; - fixture.detectChanges(); - - let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); - expect(editIcon).toBeNull('Edit icon should NOT be shown'); - }); - - it('should trigger an update event on the CardViewUpdateService', (done) => { - component.editable = true; - component.property.editable = true; - const cardViewUpdateService = TestBed.get(CardViewUpdateService); - const expectedText = 'changed text'; - fixture.detectChanges(); - - cardViewUpdateService.itemUpdated$.subscribe( - (updateNotification) => { - expect(updateNotification.target).toBe(component.property); - expect(updateNotification.changed).toEqual({ textkey: expectedText }); - done(); - } - ); - - let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-toggle-${component.property.key}"]`)); - editIcon.triggerEventHandler('click', null); - fixture.detectChanges(); - - let editInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-editinput-${component.property.key}"]`)); - editInput.nativeElement.value = expectedText; - editInput.nativeElement.dispatchEvent(new Event('input')); - fixture.detectChanges(); - - let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`)); - updateInput.triggerEventHandler('click', null); - }); - - it('should switch back to readonly mode after an update attempt', async(() => { - component.editable = true; - component.property.editable = true; - component.inEdit = true; - component.editedValue = 'updated-value'; - fixture.detectChanges(); - - component.update(); - - fixture.whenStable().then(() => { - expect(component.property.value).toBe(component.editedValue); - expect(component.inEdit).toBeFalsy(); - }); - })); -}); diff --git a/lib/core/card-view/card-view.component.html b/lib/core/card-view/card-view.component.html deleted file mode 100644 index afe045024f..0000000000 --- a/lib/core/card-view/card-view.component.html +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
- -
-
-
- diff --git a/lib/core/card-view/card-view.component.scss b/lib/core/card-view/card-view.component.scss deleted file mode 100644 index 2cac58417d..0000000000 --- a/lib/core/card-view/card-view.component.scss +++ /dev/null @@ -1,34 +0,0 @@ - -@mixin adf-card-view-theme($theme) { - - .adf { - &-property-list { - display: table; - width: 100%; - border-collapse: collapse; - border-spacing: 0; - } - - &-property { - display: table-row; - } - - &-property &-property-label { - display: table-cell; - min-width: 100px; - padding-right: 30px; - word-wrap: break-word; - color: rgb(186, 186, 186);; - vertical-align: top; - padding-bottom: 20px; - } - - &-property &-property-value { - width: 100%; - display: table-cell; - color: rgb(101, 101, 101); - vertical-align: top; - padding-bottom: 20px; - } - } -} diff --git a/lib/core/card-view/card-view.module.scss b/lib/core/card-view/card-view.module.scss new file mode 100644 index 0000000000..a3a1c75074 --- /dev/null +++ b/lib/core/card-view/card-view.module.scss @@ -0,0 +1,11 @@ +@import './components/card-view-dateitem/card-view-dateitem.component'; +@import './components/card-view-textitem/card-view-textitem.component'; +@import './components/card-view/card-view.component'; +@import '~@mat-datetimepicker/core/datetimepicker/datetimepicker-theme.scss'; + +@mixin adf-card-view-module-theme($theme) { + @include adf-card-view-dateitem-theme($theme); + @include adf-card-view-textitem-theme($theme); + @include adf-card-view-theme($theme); + @include mat-datetimepicker-theme($theme); +} diff --git a/lib/core/card-view/card-view.module.ts b/lib/core/card-view/card-view.module.ts index 4941131289..416aff18c9 100644 --- a/lib/core/card-view/card-view.module.ts +++ b/lib/core/card-view/card-view.module.ts @@ -18,47 +18,69 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatButtonModule, MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material'; +import { + MatButtonModule, + MatDatepickerModule, + MatIconModule, + MatInputModule, + MatCheckboxModule, + MatNativeDateModule +} from '@angular/material'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { FlexLayoutModule } from '@angular/flex-layout'; import { TranslateModule } from '@ngx-translate/core'; -import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; -import { CardViewDateItemComponent } from './card-view-dateitem.component'; -import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component'; -import { CardViewMapItemComponent } from './card-view-mapitem.component'; -import { CardViewTextItemComponent } from './card-view-textitem.component'; -import { CardViewComponent } from './card-view.component'; +import { CardViewContentProxyDirective } from './directives/card-view-content-proxy.directive'; +import { CardViewComponent } from './components/card-view/card-view.component'; +import { CardViewBoolItemComponent } from './components/card-view-boolitem/card-view-boolitem.component'; +import { CardViewDateItemComponent } from './components/card-view-dateitem/card-view-dateitem.component'; +import { CardViewItemDispatcherComponent } from './components/card-view-item-dispatcher/card-view-item-dispatcher.component'; +import { CardViewMapItemComponent } from './components/card-view-mapitem/card-view-mapitem.component'; +import { CardViewTextItemComponent } from './components/card-view-textitem/card-view-textitem.component'; + +import { CardItemTypeService } from './services/card-item-types.service'; +import { CardViewUpdateService } from './services/card-view-update.service'; @NgModule({ imports: [ CommonModule, + FormsModule, + FlexLayoutModule, + TranslateModule, MatDatepickerModule, MatNativeDateModule, + MatCheckboxModule, MatInputModule, MatIconModule, MatButtonModule, - FormsModule, - FlexLayoutModule, - TranslateModule + MatDatetimepickerModule, + MatNativeDatetimeModule ], declarations: [ CardViewComponent, - CardViewItemDispatcherComponent, - CardViewContentProxyDirective, - CardViewTextItemComponent, + CardViewBoolItemComponent, + CardViewDateItemComponent, CardViewMapItemComponent, - CardViewDateItemComponent + CardViewTextItemComponent, + CardViewItemDispatcherComponent, + CardViewContentProxyDirective ], entryComponents: [ - CardViewTextItemComponent, + CardViewBoolItemComponent, + CardViewDateItemComponent, CardViewMapItemComponent, - CardViewDateItemComponent + CardViewTextItemComponent ], exports: [ CardViewComponent, - CardViewTextItemComponent, + CardViewBoolItemComponent, + CardViewDateItemComponent, CardViewMapItemComponent, - CardViewDateItemComponent + CardViewTextItemComponent + ], + providers: [ + CardItemTypeService, + CardViewUpdateService ] }) export class CardViewModule {} diff --git a/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.html b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.html new file mode 100644 index 0000000000..202f179844 --- /dev/null +++ b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.html @@ -0,0 +1,10 @@ + +
{{ property.label | translate }}
+
+ + +
+
diff --git a/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.scss b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.scss new file mode 100644 index 0000000000..a94027d036 --- /dev/null +++ b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.scss @@ -0,0 +1,2 @@ +.adf { +} diff --git a/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.spec.ts b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.spec.ts new file mode 100644 index 0000000000..1e32830480 --- /dev/null +++ b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.spec.ts @@ -0,0 +1,243 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { MaterialModule } from '../../../material.module'; +import { MatCheckboxChange, MatCheckbox } from '@angular/material'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { AppConfigService } from '../../../app-config/app-config.service'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; +import { LogService } from '../../../services/log.service'; +import { TranslateLoaderService } from '../../../services/translate-loader.service'; + +import { CardViewBoolItemComponent } from './card-view-boolitem.component'; +import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model'; + +describe('CardViewBoolItemComponent', () => { + + let fixture: ComponentFixture; + let component: CardViewBoolItemComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + FormsModule, + NoopAnimationsModule, + MaterialModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderService + } + }) + ], + declarations: [ + CardViewBoolItemComponent + ], + providers: [ + AppConfigService, + CardViewUpdateService, + LogService + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CardViewBoolItemComponent); + component = fixture.componentInstance; + component.property = new CardViewBoolItemModel({ + label: 'Boolean label', + value: true, + key: 'boolkey', + default: false, + editable: false + }); + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('Rendering', () => { + + it('should render the label and value if the property is editable', () => { + component.editable = true; + component.property.editable = true; + fixture.detectChanges(); + + let label = fixture.debugElement.query(By.css('.adf-property-label')); + expect(label).not.toBeNull(); + expect(label.nativeElement.innerText).toBe('Boolean label'); + + let value = fixture.debugElement.query(By.css('.adf-property-value')); + expect(value).not.toBeNull(); + }); + + it('should NOT render the label and value if the property is NOT editable and doesn\'t have a proper boolean value set' , () => { + component.editable = true; + component.property.value = undefined; + component.property.editable = false; + fixture.detectChanges(); + + let label = fixture.debugElement.query(By.css('.adf-property-label')); + expect(label).toBeNull(); + + let value = fixture.debugElement.query(By.css('.adf-property-value')); + expect(value).toBeNull(); + }); + + it('should render the label and value if the property is NOT editable but has a proper boolean value set' , () => { + component.editable = true; + component.property.value = false; + component.property.editable = false; + fixture.detectChanges(); + + let label = fixture.debugElement.query(By.css('.adf-property-label')); + expect(label).not.toBeNull(); + + let value = fixture.debugElement.query(By.css('.adf-property-value')); + expect(value).not.toBeNull(); + }); + + it('should render ticked checkbox if property\'s value is true', () => { + component.property.value = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.checked).toBe(true); + }); + + it('should render ticked checkbox if property\'s value is not set but default is true and editable', () => { + component.editable = true; + component.property.editable = true; + component.property.value = undefined; + component.property.default = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.checked).toBe(true); + }); + + it('should render unticked checkbox if property\'s value is false', () => { + component.property.value = false; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.checked).toBe(false); + }); + + it('should render unticked checkbox if property\'s value is not set but default is false and editable', () => { + component.editable = true; + component.property.editable = true; + component.property.value = undefined; + component.property.default = false; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.checked).toBe(false); + }); + + it('should render enabled checkbox if property and component are both editable', () => { + component.editable = true; + component.property.editable = true; + component.property.value = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.hasAttribute('disabled')).toBe(false); + }); + + it('should render disabled checkbox if property is not editable', () => { + component.editable = true; + component.property.editable = false; + component.property.value = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.hasAttribute('disabled')).toBe(true); + }); + + it('should render disabled checkbox if component is not editable', () => { + component.editable = false; + component.property.editable = true; + component.property.value = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value input[type="checkbox"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.hasAttribute('disabled')).toBe(true); + }); + }); + + describe('Update', () => { + + beforeEach(() => { + component.editable = true; + component.property.editable = true; + component.property.value = undefined; + fixture.detectChanges(); + }); + + it('should trigger the update event when changing the checkbox', () => { + const cardViewUpdateService = TestBed.get(CardViewUpdateService); + spyOn(cardViewUpdateService, 'update'); + + component.changed( {checked: true}); + + expect(cardViewUpdateService.update).toHaveBeenCalledWith(component.property, true); + }); + + it('should update the propery\'s value after a changed', async(() => { + component.property.value = true; + + component.changed( {checked: false}); + + fixture.whenStable().then(() => { + expect(component.property.value).toBe(false); + }); + })); + + it('should trigger an update event on the CardViewUpdateService [integration]', (done) => { + const cardViewUpdateService = TestBed.get(CardViewUpdateService); + component.property.value = false; + fixture.detectChanges(); + + cardViewUpdateService.itemUpdated$.subscribe( + (updateNotification) => { + expect(updateNotification.target).toBe(component.property); + expect(updateNotification.changed).toEqual({ boolkey: true }); + done(); + } + ); + + const labelElement = fixture.debugElement.query(By.directive(MatCheckbox)).nativeElement.querySelector('label'); + labelElement.click(); + }); + }); +}); diff --git a/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.ts b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.ts new file mode 100644 index 0000000000..719f484479 --- /dev/null +++ b/lib/core/card-view/components/card-view-boolitem/card-view-boolitem.component.ts @@ -0,0 +1,47 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Component, Input } from '@angular/core'; +import { MatCheckboxChange } from '@angular/material'; +import { CardViewBoolItemModel } from '../../models/card-view-boolitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; + +@Component({ + selector: 'adf-card-view-boolitem', + templateUrl: './card-view-boolitem.component.html', + styleUrls: ['./card-view-boolitem.component.scss'] +}) + +export class CardViewBoolItemComponent { + + @Input() + property: CardViewBoolItemModel; + + @Input() + editable: boolean; + + constructor(private cardViewUpdateService: CardViewUpdateService) {} + + isEditable() { + return this.editable && this.property.editable; + } + + changed(change: MatCheckboxChange) { + this.cardViewUpdateService.update(this.property, change.checked ); + this.property.value = change.checked; + } +} diff --git a/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.html b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.html new file mode 100644 index 0000000000..d16f4bfb7e --- /dev/null +++ b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.html @@ -0,0 +1,37 @@ +
{{ property.label | translate }}
+
+ + + {{ property.displayValue }} + + +
+
+ + {{ property.displayValue }} + + + +
+ + + + + +
+ + {{ property.default | translate }} + +
diff --git a/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.scss b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.scss new file mode 100644 index 0000000000..17a1307e3f --- /dev/null +++ b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.scss @@ -0,0 +1,41 @@ +@mixin adf-card-view-dateitem-theme($theme) { + + .adf { + &-invisible-date-input { + height: 2px; + width: 0; + overflow: hidden; + opacity: 0; + border: none; + margin: 0; + padding: 0; + float: right; + } + + &-dateitem-editable { + cursor: pointer; + + &-controls { + display: flex; + align-items: center; + justify-content: space-between; + + button.mat-icon-button { + line-height: 20px; + height: 20px; + width: 20px; + } + + mat-icon { + width: 16px; + height: 16px; + opacity: 0.5; + } + + &:hover mat-icon { + opacity: 1; + } + } + } + } +} diff --git a/lib/core/card-view/card-view-dateitem.component.spec.ts b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts similarity index 79% rename from lib/core/card-view/card-view-dateitem.component.spec.ts rename to lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts index 19cbe653ca..3a15f3d5cd 100644 --- a/lib/core/card-view/card-view-dateitem.component.spec.ts +++ b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.spec.ts @@ -18,13 +18,14 @@ import { HttpClientModule } from '@angular/common/http'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDatepickerModule, MatInputModule, MatNativeDateModule } from '@angular/material'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import moment from 'moment-es6'; -import { AppConfigService } from '../index'; -import { CardViewDateItemModel } from '../models/card-view-dateitem.model'; -import { CardViewUpdateService } from '../services/card-view-update.service'; -import { TranslateLoaderService } from '../services/translate-loader.service'; +import { AppConfigService } from '../../../index'; +import { CardViewDateItemModel } from '../../models/card-view-dateitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; +import { TranslateLoaderService } from '../../../services/translate-loader.service'; import { CardViewDateItemComponent } from './card-view-dateitem.component'; @@ -40,6 +41,8 @@ describe('CardViewDateItemComponent', () => { MatDatepickerModule, MatInputModule, MatNativeDateModule, + MatNativeDatetimeModule, + MatDatetimepickerModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -87,7 +90,7 @@ describe('CardViewDateItemComponent', () => { expect(value.nativeElement.innerText.trim()).toBe('Jul 10 2017'); }); - it('should render the default as value if the value is empty and editable:false', () => { + it('should NOT render the default as value if the value is empty, editable:false and displayEmpty is false', () => { component.property = new CardViewDateItemModel ({ label: 'Date label', value: '', @@ -96,6 +99,26 @@ describe('CardViewDateItemComponent', () => { format: '', editable: false }); + component.editable = true; + component.displayEmpty = false; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css('.adf-property-value')); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe(''); + }); + + it('should render the default as value if the value is empty, editable:false and displayEmpty is true', () => { + component.property = new CardViewDateItemModel ({ + label: 'Date label', + value: '', + key: 'datekey', + default: 'FAKE-DEFAULT-KEY', + format: '', + editable: false + }); + component.editable = true; + component.displayEmpty = true; fixture.detectChanges(); let value = fixture.debugElement.query(By.css('.adf-property-value')); @@ -112,6 +135,7 @@ describe('CardViewDateItemComponent', () => { format: '', editable: true }); + component.editable = true; fixture.detectChanges(); let value = fixture.debugElement.query(By.css('.adf-property-value')); @@ -161,7 +185,7 @@ describe('CardViewDateItemComponent', () => { expect(datePickerToggle).toBeNull('Datepicker toggle should NOT be shown'); }); - it('should open the datetimepicker when clicking on the label', () => { + it('should open the dateXXXpicker when clicking on the label', () => { component.editable = true; component.property.editable = true; fixture.detectChanges(); @@ -190,4 +214,20 @@ describe('CardViewDateItemComponent', () => { component.onDateChanged({value: expectedDate}); }); + + it('should update the propery\'s value after a succesful update attempt', async(() => { + component.editable = true; + component.property.editable = true; + component.property.value = null; + const expectedDate = moment('Jul 10 2017', 'MMM DD YY'); + fixture.detectChanges(); + + component.onDateChanged({value: expectedDate}); + + fixture.whenStable().then( + (updateNotification) => { + expect(component.property.value).toEqual(expectedDate.toDate()); + } + ); + })); }); diff --git a/lib/core/card-view/card-view-dateitem.component.ts b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.ts similarity index 66% rename from lib/core/card-view/card-view-dateitem.component.ts rename to lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.ts index ab353da31a..09f5be4e9b 100644 --- a/lib/core/card-view/card-view-dateitem.component.ts +++ b/lib/core/card-view/components/card-view-dateitem/card-view-dateitem.component.ts @@ -16,20 +16,24 @@ */ import { Component, Input, OnInit, ViewChild } from '@angular/core'; -import { MatDatepicker } from '@angular/material'; import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material'; +import { MatDatetimepicker, DatetimeAdapter, MAT_DATETIME_FORMATS } from '@mat-datetimepicker/core'; +import { MomentDatetimeAdapter, MAT_MOMENT_DATETIME_FORMATS } from '@mat-datetimepicker/moment'; import moment from 'moment-es6'; import { Moment } from 'moment'; -import { CardViewDateItemModel } from '../models/card-view-dateitem.model'; -import { CardViewUpdateService } from '../services/card-view-update.service'; -import { UserPreferencesService } from '../services/user-preferences.service'; -import { MomentDateAdapter } from '../utils/momentDateAdapter'; -import { MOMENT_DATE_FORMATS } from '../utils/moment-date-formats.model'; +import { CardViewDateItemModel } from '../../models/card-view-dateitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; +import { UserPreferencesService } from '../../../services/user-preferences.service'; +import { MomentDateAdapter } from '../../../utils/momentDateAdapter'; +import { MOMENT_DATE_FORMATS } from '../../../utils/moment-date-formats.model'; @Component({ providers: [ { provide: DateAdapter, useClass: MomentDateAdapter }, - { provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }], + { provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }, + { provide: DatetimeAdapter, useClass: MomentDatetimeAdapter }, + { provide: MAT_DATETIME_FORMATS, useValue: MAT_MOMENT_DATETIME_FORMATS } + ], selector: 'adf-card-view-dateitem', templateUrl: './card-view-dateitem.component.html', styleUrls: ['./card-view-dateitem.component.scss'] @@ -42,10 +46,13 @@ export class CardViewDateItemComponent implements OnInit { property: CardViewDateItemModel; @Input() - editable: boolean; + editable: boolean = false; - @ViewChild(MatDatepicker) - public datepicker: MatDatepicker; + @Input() + displayEmpty: boolean = true; + + @ViewChild(MatDatetimepicker) + public datepicker: MatDatetimepicker; valueDate: Moment; @@ -58,13 +65,15 @@ export class CardViewDateItemComponent implements OnInit { this.preferences.locale$.subscribe((locale) => { this.dateAdapter.setLocale(locale); }); - let momentDateAdapter = this.dateAdapter; - momentDateAdapter.overrideDisplyaFormat = this.SHOW_FORMAT; + ( this.dateAdapter).overrideDisplyaFormat = this.SHOW_FORMAT; if (this.property.value) { this.valueDate = moment(this.property.value, this.SHOW_FORMAT); } + } + showProperty() { + return this.displayEmpty || !this.property.isEmpty(); } isEditable() { @@ -81,6 +90,7 @@ export class CardViewDateItemComponent implements OnInit { if (momentDate.isValid()) { this.valueDate = momentDate; this.cardViewUpdateService.update(this.property, momentDate.toDate()); + this.property.value = momentDate.toDate(); } } } diff --git a/lib/core/card-view/card-view-item-dispatcher.component.spec.ts b/lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.spec.ts similarity index 71% rename from lib/core/card-view/card-view-item-dispatcher.component.spec.ts rename to lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.spec.ts index 42da90102d..8201923007 100644 --- a/lib/core/card-view/card-view-item-dispatcher.component.spec.ts +++ b/lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.spec.ts @@ -17,14 +17,14 @@ /* tslint:disable:component-selector */ -import { Component, Input } from '@angular/core'; +import { Component, Input, SimpleChange } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; -import { CardViewItem } from '../interface/card-view-item.interface'; -import { CardItemTypeService } from '../services/card-item-types.service'; -import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; -import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component'; +import { CardViewItem } from '../../interfaces/card-view-item.interface'; +import { CardItemTypeService } from '../../services/card-item-types.service'; +import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive'; +import { CardViewItemDispatcherComponent } from '../card-view-item-dispatcher/card-view-item-dispatcher.component'; @Component({ selector: 'whatever-you-want-to-have', @@ -79,9 +79,10 @@ describe('CardViewItemDispatcherComponent', () => { } }; component.editable = true; + component.displayEmpty = true; fixture.detectChanges(); - component.ngOnChanges(null); + component.ngOnChanges({}); }); afterEach(() => { @@ -97,9 +98,9 @@ describe('CardViewItemDispatcherComponent', () => { }); it('should load the CardViewShinyCustomElementItemComponent only ONCE', () => { - component.ngOnChanges(); - component.ngOnChanges(); - component.ngOnChanges(); + component.ngOnChanges({}); + component.ngOnChanges({}); + component.ngOnChanges({}); fixture.detectChanges(); const shinyCustomElementItemComponent = fixture.debugElement.queryAll(By.css('whatever-you-want-to-have')); @@ -107,11 +108,32 @@ describe('CardViewItemDispatcherComponent', () => { expect(shinyCustomElementItemComponent.length).toEqual(1); }); - it('should pass through the property and editable parameters', () => { + it('should pass through the property, editable and displayEmpty parameters', () => { const shinyCustomElementItemComponent = fixture.debugElement.query(By.css('whatever-you-want-to-have')).componentInstance; expect(shinyCustomElementItemComponent.property).toBe(component.property); expect(shinyCustomElementItemComponent.editable).toBe(component.editable); + expect(shinyCustomElementItemComponent.displayEmpty).toBe(component.displayEmpty); + }); + + it('should update the subcomponent\'s input parameters', () => { + const expectedEditable = false, + expectedDisplayEmpty = true, + expectedProperty = {}, + expectedCustomInput = 1; + + component.ngOnChanges({ + editable: new SimpleChange(true, expectedEditable, false), + displayEmpty: new SimpleChange(false, expectedDisplayEmpty, false), + property: new SimpleChange(null, expectedProperty, false), + customInput: new SimpleChange(0, expectedCustomInput, false) + }); + + const shinyCustomElementItemComponent = fixture.debugElement.query(By.css('whatever-you-want-to-have')).componentInstance; + expect(shinyCustomElementItemComponent.property).toBe(expectedProperty); + expect(shinyCustomElementItemComponent.editable).toBe(expectedEditable); + expect(shinyCustomElementItemComponent.displayEmpty).toBe(expectedDisplayEmpty); + expect(shinyCustomElementItemComponent.customInput).toBe(expectedCustomInput); }); }); @@ -138,21 +160,21 @@ describe('CardViewItemDispatcherComponent', () => { lifeCycleMethods.forEach((lifeCycleMethod) => { shinyCustomElementItemComponent[lifeCycleMethod] = () => {}; spyOn(shinyCustomElementItemComponent, lifeCycleMethod); - const param1 = {}; - const param2 = {}; + const param = {}; - component[lifeCycleMethod].call(component, param1, param2); + component[lifeCycleMethod].call(component, param); - expect(shinyCustomElementItemComponent[lifeCycleMethod]).toHaveBeenCalledWith(param1, param2); + expect(shinyCustomElementItemComponent[lifeCycleMethod]).toHaveBeenCalledWith(param); }); }); it('should NOT call through the lifecycle methods if the method does not exist (no error should be thrown)', () => { + const param = {}; lifeCycleMethods.forEach((lifeCycleMethod) => { shinyCustomElementItemComponent[lifeCycleMethod] = undefined; const execution = () => { - component[lifeCycleMethod].call(component); + component[lifeCycleMethod].call(component, param); }; expect(execution).not.toThrowError(); diff --git a/lib/core/card-view/card-view-item-dispatcher.component.ts b/lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts similarity index 76% rename from lib/core/card-view/card-view-item-dispatcher.component.ts rename to lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts index c8b3ea13c0..338d1cf3a7 100644 --- a/lib/core/card-view/card-view-item-dispatcher.component.ts +++ b/lib/core/card-view/components/card-view-item-dispatcher/card-view-item-dispatcher.component.ts @@ -20,11 +20,13 @@ import { ComponentFactoryResolver, Input, OnChanges, + SimpleChange, + SimpleChanges, ViewChild } from '@angular/core'; -import { CardViewItem } from '../interface/card-view-item.interface'; -import { CardItemTypeService } from '../services/card-item-types.service'; -import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; +import { CardViewItem } from '../../interfaces/card-view-item.interface'; +import { CardItemTypeService } from '../../services/card-item-types.service'; +import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive'; @Component({ selector: 'adf-card-view-item-dispatcher', @@ -37,6 +39,9 @@ export class CardViewItemDispatcherComponent implements OnChanges { @Input() editable: boolean; + @Input() + displayEmpty: boolean = true; + @ViewChild(CardViewContentProxyDirective) private content: CardViewContentProxyDirective; @@ -63,13 +68,19 @@ export class CardViewItemDispatcherComponent implements OnChanges { }); } - ngOnChanges(...args) { + ngOnChanges(changes: SimpleChanges) { if (!this.loaded) { this.loadComponent(); this.loaded = true; } - this.proxy('ngOnChanges', ...args); + Object.keys(changes) + .map(changeName => [changeName, changes[changeName]]) + .forEach(([inputParamName, simpleChange]: [string, SimpleChange]) => { + this.componentReference.instance[inputParamName] = simpleChange.currentValue; + }); + + this.proxy('ngOnChanges', changes); } private loadComponent() { @@ -80,6 +91,7 @@ export class CardViewItemDispatcherComponent implements OnChanges { this.componentReference.instance.editable = this.editable; this.componentReference.instance.property = this.property; + this.componentReference.instance.displayEmpty = this.displayEmpty; } private proxy(methodName, ...args) { diff --git a/lib/core/card-view/card-view-mapitem.component.html b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.html similarity index 63% rename from lib/core/card-view/card-view-mapitem.component.html rename to lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.html index 01021e0348..82f9749b3c 100644 --- a/lib/core/card-view/card-view-mapitem.component.html +++ b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.html @@ -1,12 +1,12 @@ -
{{ property.label | translate }}
+
{{ property.label | translate }}
- {{ property.displayValue }} + {{ property.displayValue }} - {{ property.displayValue }} + {{ property.displayValue }}
diff --git a/lib/core/card-view/card-view-mapitem.component.scss b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.scss similarity index 100% rename from lib/core/card-view/card-view-mapitem.component.scss rename to lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.scss diff --git a/lib/core/card-view/card-view-mapitem.component.spec.ts b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.spec.ts similarity index 77% rename from lib/core/card-view/card-view-mapitem.component.spec.ts rename to lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.spec.ts index e9e3a0b480..814cdc89f4 100644 --- a/lib/core/card-view/card-view-mapitem.component.spec.ts +++ b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.spec.ts @@ -19,15 +19,15 @@ import { HttpClientModule } from '@angular/common/http'; import { DebugElement } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; -import { MaterialModule } from '../material.module'; +import { MaterialModule } from '../../../material.module'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { CardViewMapItemModel } from '../models/card-view-mapitem.model'; -import { AppConfigService } from '../app-config/app-config.service'; -import { CardViewUpdateService } from '../services/card-view-update.service'; -import { LogService } from '../services/log.service'; -import { TranslateLoaderService } from '../services/translate-loader.service'; +import { CardViewMapItemModel } from '../../models/card-view-mapitem.model'; +import { AppConfigService } from '../../../app-config/app-config.service'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; +import { LogService } from '../../../services/log.service'; +import { TranslateLoaderService } from '../../../services/translate-loader.service'; import { CardViewMapItemComponent } from './card-view-mapitem.component'; @@ -77,14 +77,15 @@ describe('CardViewMapItemComponent', () => { TestBed.resetTestingModule(); }); - it('should render the default if the value is empty', () => { + it('should render the default if the value is empty and displayEmpty is true', () => { component.property = new CardViewMapItemModel({ label: 'Map label', value: null, key: 'mapkey', - default: 'Fake default' + default: 'Fake default', + clickable: false }); - + component.displayEmpty = true; fixture.detectChanges(); let labelValue = debug.query(By.css('.adf-property-label')); @@ -96,6 +97,25 @@ describe('CardViewMapItemComponent', () => { expect(value.nativeElement.innerText.trim()).toBe('Fake default'); }); + it('should NOT render the default if the value is empty and displayEmpty is false', () => { + component.property = new CardViewMapItemModel({ + label: 'Map label', + value: null, + key: 'mapkey', + default: 'Fake default', + clickable: false + }); + component.displayEmpty = false; + fixture.detectChanges(); + + let labelValue = debug.query(By.css('.adf-property-label')); + expect(labelValue).toBeNull(); + + let value = debug.query(By.css(`[data-automation-id="card-mapitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe(''); + }); + it('should render the label and value', () => { component.property = new CardViewMapItemModel({ label: 'Map label', diff --git a/lib/core/card-view/card-view-mapitem.component.ts b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.ts similarity index 79% rename from lib/core/card-view/card-view-mapitem.component.ts rename to lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.ts index a6a4f30641..c325db0c3c 100644 --- a/lib/core/card-view/card-view-mapitem.component.ts +++ b/lib/core/card-view/components/card-view-mapitem/card-view-mapitem.component.ts @@ -16,8 +16,8 @@ */ import { Component, Input } from '@angular/core'; -import { CardViewMapItemModel } from '../models/card-view-mapitem.model'; -import { CardViewUpdateService } from '../services/card-view-update.service'; +import { CardViewMapItemModel } from '../../models/card-view-mapitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; @Component({ selector: 'adf-card-view-mapitem', @@ -29,8 +29,15 @@ export class CardViewMapItemComponent { @Input() property: CardViewMapItemModel; + @Input() + displayEmpty: boolean = true; + constructor(private cardViewUpdateService: CardViewUpdateService) {} + showProperty() { + return this.displayEmpty || !this.property.isEmpty(); + } + isClickable() { return this.property.clickable; } diff --git a/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.html b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.html new file mode 100644 index 0000000000..9947abc706 --- /dev/null +++ b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.html @@ -0,0 +1,58 @@ +
{{ property.label | translate }}
+
+ + + {{ property.displayValue }} + + + + {{ property.displayValue }} + + + + +
+ + {{ property.displayValue }} + + create +
+
+
+ + + + + done + clear +
+ +
    +
  • {{ errorMessage | translate }}
  • +
+
+
+
+ + {{ property.default | translate }} + +
diff --git a/lib/core/card-view/card-view-textitem.component.scss b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.scss similarity index 66% rename from lib/core/card-view/card-view-textitem.component.scss rename to lib/core/card-view/components/card-view-textitem/card-view-textitem.component.scss index a803682fad..58b5209f1c 100644 --- a/lib/core/card-view/card-view-textitem.component.scss +++ b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.scss @@ -30,23 +30,46 @@ } &-textitem-editable { - display: flex; - mat-icon:hover { - opacity: 1; - cursor: pointer; + &-controls { + display: flex; + + mat-icon:hover { + opacity: 1; + cursor: pointer; + } + + mat-form-field { + width: 100%; + } + + input:focus, + textarea:focus { + border: 1px solid mat-color($foreground, text, 0.15); + } } - mat-form-field { - width: 100%; - } + &-error { + font-size: 12px; + padding-top: 4px; - input:focus, - textarea:focus { - border: 1px solid mat-color($foreground, text, 0.15); + ul { + margin: 0; + padding: 0; + list-style-type: none; + + li { + margin: 0; + padding: 0; + } + } } } + &-textitem-default-value { + color: mat-color($foreground, text, 0.54); + } + &-textitem-editable .mat-input-wrapper { margin: 0; padding-bottom: 0; @@ -67,7 +90,7 @@ } &-textitem-editable .mat-input-placeholder { - top: 0; + top: 4px; } &-textitem-editable .mat-input-element { diff --git a/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts new file mode 100644 index 0000000000..7aeee87c86 --- /dev/null +++ b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.spec.ts @@ -0,0 +1,325 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { CardViewTextItemModel } from '../../models/card-view-textitem.model'; +import { AppConfigService } from '../../../app-config/app-config.service'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; +import { LogService } from '../../../services/log.service'; +import { TranslateLoaderService } from '../../../services/translate-loader.service'; + +import { CardViewTextItemComponent } from './card-view-textitem.component'; + +describe('CardViewTextItemComponent', () => { + + let fixture: ComponentFixture; + let component: CardViewTextItemComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + FormsModule, + NoopAnimationsModule, + MatDatepickerModule, + MatIconModule, + MatInputModule, + MatNativeDateModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderService + } + }) + ], + declarations: [ + CardViewTextItemComponent + ], + providers: [ + AppConfigService, + CardViewUpdateService, + LogService + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CardViewTextItemComponent); + component = fixture.componentInstance; + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: 'Lorem ipsum', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + editable: false + }); + }); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + describe('Rendering', () => { + + it('should render the label and value', () => { + fixture.detectChanges(); + + let labelValue = fixture.debugElement.query(By.css('.adf-property-label')); + expect(labelValue).not.toBeNull(); + expect(labelValue.nativeElement.innerText).toBe('Text label'); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum'); + }); + + it('should NOT render the default as value if the value is empty, editable is false and displayEmpty is false', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + editable: false + }); + component.displayEmpty = false; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe(''); + }); + + it('should render the default as value if the value is empty, editable is false and displayEmpty is true', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + editable: false + }); + component.displayEmpty = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); + }); + + it('should render the default as value if the value is empty and editable true', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + editable: true + }); + component.editable = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); + }); + + it('should NOT render the default as value if the value is empty, clickable is false and displayEmpty is false', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + clickable: false + }); + component.displayEmpty = false; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe(''); + }); + + it('should render the default as value if the value is empty, clickable is false and displayEmpty is true', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + clickable: false + }); + component.displayEmpty = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); + }); + + it('should render the default as value if the value is empty and clickable true', () => { + component.property = new CardViewTextItemModel ({ + label: 'Text label', + value: '', + key: 'textkey', + default: 'FAKE-DEFAULT-KEY', + clickable: true + }); + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('FAKE-DEFAULT-KEY'); + }); + + it('should render value when editable:true', () => { + component.editable = true; + component.property.editable = true; + fixture.detectChanges(); + + let value = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-value-${component.property.key}"]`)); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('Lorem ipsum'); + }); + + it('should render the edit icon in case of editable:true', () => { + component.editable = true; + component.property.editable = true; + fixture.detectChanges(); + + let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); + expect(editIcon).not.toBeNull('Edit icon should be shown'); + }); + + it('should NOT render the edit icon in case of editable:false', () => { + component.editable = false; + fixture.detectChanges(); + + let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); + expect(editIcon).toBeNull('Edit icon should NOT be shown'); + }); + + it('should NOT render the picker and toggle in case of editable:true but (general) editable:false', () => { + component.editable = false; + component.property.editable = true; + fixture.detectChanges(); + + let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-icon-${component.property.key}"]`)); + expect(editIcon).toBeNull('Edit icon should NOT be shown'); + }); + }); + + describe('Update', () => { + + beforeEach(() => { + component.editable = true; + component.property.editable = true; + component.inEdit = true; + component.editedValue = 'updated-value'; + fixture.detectChanges(); + }); + + it('should call the isValid method with the edited value', () => { + spyOn(component.property, 'isValid'); + component.editedValue = 'updated-value'; + + component.update(); + + expect(component.property.isValid).toHaveBeenCalledWith('updated-value'); + }); + + it('should trigger the update event if the editedValue is valid', () => { + component.property.isValid = () => true; + const cardViewUpdateService = TestBed.get(CardViewUpdateService); + spyOn(cardViewUpdateService, 'update'); + component.editedValue = 'updated-value'; + + component.update(); + + expect(cardViewUpdateService.update).toHaveBeenCalledWith(component.property, 'updated-value'); + }); + + it('should NOT trigger the update event if the editedValue is invalid', () => { + component.property.isValid = () => false; + const cardViewUpdateService = TestBed.get(CardViewUpdateService); + spyOn(cardViewUpdateService, 'update'); + + component.update(); + + expect(cardViewUpdateService.update).not.toHaveBeenCalled(); + }); + + it('should set the errorMessages properly if the editedValue is invalid', () => { + const expectedErrorMessages = ['Something went wrong']; + component.property.isValid = () => false; + component.property.getValidationErrors = () => expectedErrorMessages; + + component.update(); + + expect(component.errorMessages).toBe(expectedErrorMessages); + }); + + it('should update the propery\'s value after a succesful update attempt', async(() => { + component.property.isValid = () => true; + component.update(); + + fixture.whenStable().then(() => { + expect(component.property.value).toBe(component.editedValue); + }); + })); + + it('should switch back to readonly mode after an update attempt', async(() => { + component.property.isValid = () => true; + component.update(); + + fixture.whenStable().then(() => { + expect(component.inEdit).toBeFalsy(); + }); + })); + + it('should trigger an update event on the CardViewUpdateService [integration]', (done) => { + component.inEdit = false; + component.property.isValid = () => true; + const cardViewUpdateService = TestBed.get(CardViewUpdateService); + const expectedText = 'changed text'; + fixture.detectChanges(); + + cardViewUpdateService.itemUpdated$.subscribe( + (updateNotification) => { + expect(updateNotification.target).toBe(component.property); + expect(updateNotification.changed).toEqual({ textkey: expectedText }); + done(); + } + ); + + let editIcon = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-edit-toggle-${component.property.key}"]`)); + editIcon.triggerEventHandler('click', null); + fixture.detectChanges(); + + let editInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-editinput-${component.property.key}"]`)); + editInput.nativeElement.value = expectedText; + editInput.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + let updateInput = fixture.debugElement.query(By.css(`[data-automation-id="card-textitem-update-${component.property.key}"]`)); + updateInput.triggerEventHandler('click', null); + }); + }); +}); diff --git a/lib/core/card-view/card-view-textitem.component.ts b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.ts similarity index 63% rename from lib/core/card-view/card-view-textitem.component.ts rename to lib/core/card-view/components/card-view-textitem/card-view-textitem.component.ts index 6e9aea1826..015aebca63 100644 --- a/lib/core/card-view/card-view-textitem.component.ts +++ b/lib/core/card-view/components/card-view-textitem/card-view-textitem.component.ts @@ -16,8 +16,8 @@ */ import { Component, Input, OnChanges, ViewChild } from '@angular/core'; -import { CardViewTextItemModel } from '../models/card-view-textitem.model'; -import { CardViewUpdateService } from '../services/card-view-update.service'; +import { CardViewTextItemModel } from '../../models/card-view-textitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; @Component({ selector: 'adf-card-view-textitem', @@ -29,32 +29,46 @@ export class CardViewTextItemComponent implements OnChanges { property: CardViewTextItemModel; @Input() - editable: boolean; + editable: boolean = false; + + @Input() + displayEmpty: boolean = true; @ViewChild('editorInput') private editorInput: any; inEdit: boolean = false; editedValue: string; + errorMessages: string[]; constructor(private cardViewUpdateService: CardViewUpdateService) {} - ngOnChanges() { + ngOnChanges(): void { this.editedValue = this.property.value; } - isEditble() { + showProperty(): boolean { + return this.displayEmpty || !this.property.isEmpty(); + } + + isEditable(): boolean { return this.editable && this.property.editable; } - isClickable() { + isClickable(): boolean { return this.property.clickable; } + hasErrors(): number { + return this.errorMessages && this.errorMessages.length; + } + setEditMode(editStatus: boolean): void { this.inEdit = editStatus; setTimeout(() => { - this.editorInput.nativeElement.click(); + if (this.editorInput) { + this.editorInput.nativeElement.click(); + } }, 0); } @@ -64,9 +78,13 @@ export class CardViewTextItemComponent implements OnChanges { } update(): void { - this.cardViewUpdateService.update(this.property, this.editedValue ); - this.property.value = this.editedValue; - this.setEditMode(false); + if (this.property.isValid(this.editedValue)) { + this.cardViewUpdateService.update(this.property, this.editedValue ); + this.property.value = this.editedValue; + this.setEditMode(false); + } else { + this.errorMessages = this.property.getValidationErrors(this.editedValue); + } } clicked(): void { diff --git a/lib/core/card-view/components/card-view.components.ts b/lib/core/card-view/components/card-view.components.ts new file mode 100644 index 0000000000..3187f44ff6 --- /dev/null +++ b/lib/core/card-view/components/card-view.components.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './card-view/card-view.component'; +export * from './card-view-boolitem/card-view-boolitem.component'; +export * from './card-view-dateitem/card-view-dateitem.component'; +export * from './card-view-item-dispatcher/card-view-item-dispatcher.component'; +export * from './card-view-mapitem/card-view-mapitem.component'; +export * from './card-view-textitem/card-view-textitem.component'; diff --git a/lib/core/card-view/components/card-view/card-view.component.html b/lib/core/card-view/components/card-view/card-view.component.html new file mode 100644 index 0000000000..e362d0552f --- /dev/null +++ b/lib/core/card-view/components/card-view/card-view.component.html @@ -0,0 +1,11 @@ +
+
+
+ + +
+
+
diff --git a/lib/core/card-view/components/card-view/card-view.component.scss b/lib/core/card-view/components/card-view/card-view.component.scss new file mode 100644 index 0000000000..04b07cbb11 --- /dev/null +++ b/lib/core/card-view/components/card-view/card-view.component.scss @@ -0,0 +1,22 @@ + +@mixin adf-card-view-theme($theme) { + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + + .adf-property-list { + .adf-property { + margin-bottom: 20px; + + .adf-property-label { + font-size: 12px; + color: mat-color($foreground, text, 0.4); + word-wrap: break-word; + } + + .adf-property-value { + font-size: 14px; + color: mat-color($foreground, text, 0.87); + } + } + } +} diff --git a/lib/core/card-view/card-view.component.spec.ts b/lib/core/card-view/components/card-view/card-view.component.spec.ts similarity index 61% rename from lib/core/card-view/card-view.component.spec.ts rename to lib/core/card-view/components/card-view/card-view.component.spec.ts index 03f28b6eed..6e6c351079 100644 --- a/lib/core/card-view/card-view.component.spec.ts +++ b/lib/core/card-view/components/card-view/card-view.component.spec.ts @@ -20,24 +20,26 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { MatDatepickerModule, MatIconModule, MatInputModule, MatNativeDateModule } from '@angular/material'; +import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { By } from '@angular/platform-browser'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { AppConfigService } from '../app-config/app-config.service'; +import { AppConfigService } from '../../../app-config/app-config.service'; -import { CardViewDateItemModel } from '../models/card-view-dateitem.model'; -import { CardViewTextItemModel } from '../models/card-view-textitem.model'; -import { CardViewUpdateService } from '../services/card-view-update.service'; +import { CardViewDateItemModel } from '../../models/card-view-dateitem.model'; +import { CardViewTextItemModel } from '../../models/card-view-textitem.model'; +import { CardViewUpdateService } from '../../services/card-view-update.service'; -import { TranslateLoaderService } from '../services/translate-loader.service'; -import { CardViewContentProxyDirective } from './card-view-content-proxy.directive'; -import { CardViewDateItemComponent } from './card-view-dateitem.component'; -import { CardViewItemDispatcherComponent } from './card-view-item-dispatcher.component'; -import { CardViewTextItemComponent } from './card-view-textitem.component'; +import { TranslateLoaderService } from '../../../services/translate-loader.service'; +import { CardViewContentProxyDirective } from '../../directives/card-view-content-proxy.directive'; +import { CardViewDateItemComponent } from '../card-view-dateitem/card-view-dateitem.component'; +import { CardItemTypeService } from '../../services/card-item-types.service'; +import { CardViewItemDispatcherComponent } from '../card-view-item-dispatcher/card-view-item-dispatcher.component'; +import { CardViewTextItemComponent } from '../card-view-textitem/card-view-textitem.component'; import { CardViewComponent } from './card-view.component'; -describe('AdfCardView', () => { +describe('CardViewComponent', () => { let fixture: ComponentFixture; let component: CardViewComponent; @@ -50,6 +52,8 @@ describe('AdfCardView', () => { MatIconModule, MatInputModule, MatNativeDateModule, + MatDatetimepickerModule, + MatNativeDatetimeModule, FormsModule, TranslateModule.forRoot({ loader: { @@ -66,6 +70,7 @@ describe('AdfCardView', () => { CardViewDateItemComponent ], providers: [ + CardItemTypeService, CardViewUpdateService, AppConfigService ] @@ -135,14 +140,42 @@ describe('AdfCardView', () => { }); })); - it('should render the default value if the value is empty', async(() => { + it('should NOT render anything if the value is empty, not editable and displayEmpty is false', async(() => { component.properties = [new CardViewTextItemModel({ label: 'My default label', value: null, default: 'default value', - key: 'some key' + key: 'some-key', + editable: false })]; + component.editable = true; + component.displayEmpty = false; fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + let labelValue = fixture.debugElement.query(By.css('.adf-property-label')); + expect(labelValue).toBeNull(); + + let value = fixture.debugElement.query(By.css('.adf-property-value')); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe(''); + }); + })); + + it('should render the default value if the value is empty, not editable and displayEmpty is true', async(() => { + component.properties = [new CardViewTextItemModel({ + label: 'My default label', + value: null, + default: 'default value', + key: 'some-key', + editable: false + })]; + component.editable = true; + component.displayEmpty = true; + fixture.detectChanges(); + fixture.whenStable().then(() => { fixture.detectChanges(); @@ -150,9 +183,34 @@ describe('AdfCardView', () => { expect(labelValue).not.toBeNull(); expect(labelValue.nativeElement.innerText).toBe('My default label'); - let value = fixture.debugElement.query(By.css('.adf-property-value')); + let value = fixture.debugElement.query(By.css('.adf-property-value [data-automation-id="card-textitem-value-some-key"]')); expect(value).not.toBeNull(); - expect(value.nativeElement.innerText).toBe('default value'); + expect(value.nativeElement.innerText.trim()).toBe('default value'); + }); + })); + + it('should render the default value if the value is empty and is editable', async(() => { + component.properties = [new CardViewTextItemModel({ + label: 'My default label', + value: null, + default: 'default value', + key: 'some-key', + editable: true + })]; + component.editable = true; + component.displayEmpty = false; + fixture.detectChanges(); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + + let labelValue = fixture.debugElement.query(By.css('.adf-property-label')); + expect(labelValue).not.toBeNull(); + expect(labelValue.nativeElement.innerText).toBe('My default label'); + + let value = fixture.debugElement.query(By.css('.adf-property-value [data-automation-id="card-textitem-value-some-key"]')); + expect(value).not.toBeNull(); + expect(value.nativeElement.innerText.trim()).toBe('default value'); }); })); }); diff --git a/lib/core/card-view/card-view.component.ts b/lib/core/card-view/components/card-view/card-view.component.ts similarity index 88% rename from lib/core/card-view/card-view.component.ts rename to lib/core/card-view/components/card-view/card-view.component.ts index 06ef49d618..d945c61cc8 100644 --- a/lib/core/card-view/card-view.component.ts +++ b/lib/core/card-view/components/card-view/card-view.component.ts @@ -16,7 +16,7 @@ */ import { Component, Input } from '@angular/core'; -import { CardViewItem } from '../interface/card-view-item.interface'; +import { CardViewItem } from '../../interfaces/card-view-item.interface'; @Component({ selector: 'adf-card-view', @@ -29,4 +29,7 @@ export class CardViewComponent { @Input() editable: boolean; + + @Input() + displayEmpty: boolean = true; } diff --git a/lib/core/card-view/card-view-content-proxy.directive.ts b/lib/core/card-view/directives/card-view-content-proxy.directive.ts similarity index 100% rename from lib/core/card-view/card-view-content-proxy.directive.ts rename to lib/core/card-view/directives/card-view-content-proxy.directive.ts diff --git a/lib/core/card-view/interfaces/card-view-boolitem-properties.interface.ts b/lib/core/card-view/interfaces/card-view-boolitem-properties.interface.ts new file mode 100644 index 0000000000..99be6e2d78 --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-boolitem-properties.interface.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemProperties } from './card-view-item-properties.interface'; + +export interface CardViewBoolItemProperties extends CardViewItemProperties { + value: any; + default?: boolean; +} diff --git a/lib/core/card-view/interfaces/card-view-dateitem-properties.interface.ts b/lib/core/card-view/interfaces/card-view-dateitem-properties.interface.ts new file mode 100644 index 0000000000..d96c22acaf --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-dateitem-properties.interface.ts @@ -0,0 +1,22 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemProperties } from './card-view-item-properties.interface'; + +export interface CardViewDateItemProperties extends CardViewItemProperties { + format?: string; +} diff --git a/lib/core/card-view/interfaces/card-view-item-properties.interface.ts b/lib/core/card-view/interfaces/card-view-item-properties.interface.ts new file mode 100644 index 0000000000..7561d776bc --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-item-properties.interface.ts @@ -0,0 +1,28 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemValidator } from './card-view-item-validator.interface'; + +export interface CardViewItemProperties { + label: string; + value: any; + key: any; + default?: any; + editable?: boolean; + clickable?: boolean; + validators?: CardViewItemValidator[]; +} diff --git a/lib/core/card-view/interfaces/card-view-item-validator.interface.ts b/lib/core/card-view/interfaces/card-view-item-validator.interface.ts new file mode 100644 index 0000000000..78f0c7c498 --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-item-validator.interface.ts @@ -0,0 +1,21 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export interface CardViewItemValidator { + message: string; + isValid(value: any): boolean; +} diff --git a/lib/core/interface/card-view-item.interface.ts b/lib/core/card-view/interfaces/card-view-item.interface.ts similarity index 96% rename from lib/core/interface/card-view-item.interface.ts rename to lib/core/card-view/interfaces/card-view-item.interface.ts index b3496f234e..38cb44ade9 100644 --- a/lib/core/interface/card-view-item.interface.ts +++ b/lib/core/card-view/interfaces/card-view-item.interface.ts @@ -21,6 +21,6 @@ export interface CardViewItem { key: string; default?: any; type: string; - displayValue: string; + displayValue: any; editable?: boolean; } diff --git a/lib/core/card-view/interfaces/card-view-textitem-pipe-property.interface.ts b/lib/core/card-view/interfaces/card-view-textitem-pipe-property.interface.ts new file mode 100644 index 0000000000..e5284a5381 --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-textitem-pipe-property.interface.ts @@ -0,0 +1,23 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { PipeTransform } from '@angular/core'; + +export interface CardViewTextItemPipeProperty { + pipe: PipeTransform; + params?: any[]; +} diff --git a/lib/core/card-view/interfaces/card-view-textitem-properties.interface.ts b/lib/core/card-view/interfaces/card-view-textitem-properties.interface.ts new file mode 100644 index 0000000000..5ceb9cfa42 --- /dev/null +++ b/lib/core/card-view/interfaces/card-view-textitem-properties.interface.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemProperties } from './card-view-item-properties.interface'; +import { CardViewTextItemPipeProperty } from './card-view-textitem-pipe-property.interface'; + +export interface CardViewTextItemProperties extends CardViewItemProperties { + multiline?: boolean; + pipes?: CardViewTextItemPipeProperty[]; +} diff --git a/lib/core/card-view/interfaces/card-view.interfaces.ts b/lib/core/card-view/interfaces/card-view.interfaces.ts new file mode 100644 index 0000000000..a28204cf4c --- /dev/null +++ b/lib/core/card-view/interfaces/card-view.interfaces.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './card-view-item-properties.interface'; +export * from './card-view-item-validator.interface'; +export * from './card-view-item.interface'; +export * from './card-view-textitem-properties.interface'; +export * from './card-view-dateitem-properties.interface'; +export * from './card-view-boolitem-properties.interface'; +export * from './card-view-textitem-pipe-property.interface'; diff --git a/lib/core/card-view/models/card-view-baseitem.model.spec.ts b/lib/core/card-view/models/card-view-baseitem.model.spec.ts new file mode 100644 index 0000000000..0ae5f559e9 --- /dev/null +++ b/lib/core/card-view/models/card-view-baseitem.model.spec.ts @@ -0,0 +1,85 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemProperties } from '../interfaces/card-view.interfaces'; +import { CardViewBaseItemModel } from './card-view-baseitem.model'; +import { CardViewItemValidator } from '../interfaces/card-view.interfaces'; + +class CarViewCustomItemModel extends CardViewBaseItemModel {} + +describe('CardViewBaseItemModel', () => { + + let properties: CardViewItemProperties; + + beforeEach(() => { + properties = { + label: 'Tribe', + value: 'Oseram', + key: 'tribe' + }; + }); + + describe('isValid & Validation errors', () => { + + it('should be true when no validators are set', () => { + const itemModel = new CarViewCustomItemModel(properties); + + const isValid = itemModel.isValid(null); + + expect(isValid).toBe(true); + }); + + it('should call the registered validators to validate', () => { + const validator1: CardViewItemValidator = { isValid: () => true, message: 'validator 1' }; + const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' }; + spyOn(validator1, 'isValid'); + spyOn(validator2, 'isValid'); + properties.validators = [ validator1, validator2 ]; + const itemModel = new CarViewCustomItemModel(properties); + + itemModel.isValid('test-against-this'); + + expect(validator1.isValid).toHaveBeenCalledWith('test-against-this'); + expect(validator2.isValid).toHaveBeenCalledWith('test-against-this'); + }); + + it('should return the registered validators\' common decision (case true)', () => { + const validator1: CardViewItemValidator = { isValid: () => true, message: 'validator 1' }; + const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' }; + properties.validators = [ validator1, validator2 ]; + const itemModel = new CarViewCustomItemModel(properties); + + const isValid = itemModel.isValid('test-against-this'); + + expect(isValid).toBe(true); + expect(itemModel.getValidationErrors('test-against-this')).toEqual([]); + }); + + it('should return the registered validators\' common decision (case false)', () => { + const validator1: CardViewItemValidator = { isValid: () => false, message: 'validator 1' }; + const validator2: CardViewItemValidator = { isValid: () => true, message: 'validator 2' }; + const validator3: CardViewItemValidator = { isValid: () => false, message: 'validator 3' }; + properties.validators = [ validator1, validator2, validator3 ]; + const itemModel = new CarViewCustomItemModel(properties); + + const isValid = itemModel.isValid('test-against-this'); + + expect(isValid).toBe(false); + expect(itemModel.getValidationErrors('test-against-this')).toEqual(['validator 1', 'validator 3']); + }); + }); +}); diff --git a/lib/core/models/card-view-baseitem.model.ts b/lib/core/card-view/models/card-view-baseitem.model.ts similarity index 59% rename from lib/core/models/card-view-baseitem.model.ts rename to lib/core/card-view/models/card-view-baseitem.model.ts index d4f2432796..beb5ecb68e 100644 --- a/lib/core/models/card-view-baseitem.model.ts +++ b/lib/core/card-view/models/card-view-baseitem.model.ts @@ -15,30 +15,16 @@ * limitations under the License. */ -/** - * - * This object represent the basic structure of a card view. - * - * - * @returns {CardViewBaseItemModel} . - */ - -export interface CardViewItemProperties { - label: string; - value: any; - key: any; - default?: string; - editable?: boolean; - clickable?: boolean; -} +import { CardViewItemProperties, CardViewItemValidator } from '../interfaces/card-view.interfaces'; export abstract class CardViewBaseItemModel { label: string; value: any; key: any; - default: string; + default: any; editable: boolean; clickable: boolean; + validators?: CardViewItemValidator[]; constructor(obj: CardViewItemProperties) { this.label = obj.label || ''; @@ -47,9 +33,30 @@ export abstract class CardViewBaseItemModel { this.default = obj.default; this.editable = !!obj.editable; this.clickable = !!obj.clickable; + this.validators = obj.validators || []; } isEmpty(): boolean { return this.value === undefined || this.value === null || this.value === ''; } + + isValid(newValue: any): boolean { + if (!this.validators.length) { + return true; + } + + return this.validators + .map((validator) => validator.isValid(newValue)) + .reduce((isValidUntilNow, isValid) => isValidUntilNow && isValid, true); + } + + getValidationErrors(value): string[] { + if (!this.validators.length) { + return []; + } + + return this.validators + .filter((validator) => !validator.isValid(value)) + .map((validator) => validator.message); + } } diff --git a/lib/core/card-view/models/card-view-boolitem.model.spec.ts b/lib/core/card-view/models/card-view-boolitem.model.spec.ts new file mode 100644 index 0000000000..835423f0e4 --- /dev/null +++ b/lib/core/card-view/models/card-view-boolitem.model.spec.ts @@ -0,0 +1,88 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewBoolItemModel } from './card-view-boolitem.model'; +import { CardViewBoolItemProperties } from '../interfaces/card-view.interfaces'; + +describe('CardViewFloatItemModel', () => { + + let properties: CardViewBoolItemProperties; + + beforeEach(() => { + properties = { + label: 'Tribe', + value: undefined, + key: 'tribe' + }; + }); + + it('true should be parsed as true', () => { + properties.value = true; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(true); + }); + + it('"true" should be parsed as true', () => { + properties.value = 'true'; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(true); + }); + + it('1 should be parsed as true', () => { + properties.value = 1; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(true); + }); + + it('"1" should be parsed as true', () => { + properties.value = '1'; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(true); + }); + + it('"false" should be parsed as false', () => { + properties.value = 'false'; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(false); + }); + + it('false should be parsed as false', () => { + properties.value = 'false'; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(false); + }); + + it('undefined should be parsed as false', () => { + properties.value = undefined; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(false); + }); + + it('null should be parsed as false', () => { + properties.value = null; + const itemModel = new CardViewBoolItemModel(properties); + + expect(itemModel.value).toBe(false); + }); +}); diff --git a/lib/core/card-view/models/card-view-boolitem.model.ts b/lib/core/card-view/models/card-view-boolitem.model.ts new file mode 100644 index 0000000000..aef558aac8 --- /dev/null +++ b/lib/core/card-view/models/card-view-boolitem.model.ts @@ -0,0 +1,43 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewBaseItemModel } from './card-view-baseitem.model'; +import { CardViewBoolItemProperties } from '../interfaces/card-view.interfaces'; + +export class CardViewBoolItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { + type: string = 'bool'; + value: boolean = false; + default: boolean; + + constructor(obj: CardViewBoolItemProperties) { + super(obj); + + if (obj.value !== undefined) { + this.value = !!JSON.parse(obj.value); + } + } + + get displayValue() { + if (this.isEmpty()) { + return this.default; + } else { + return this.value; + } + } +} diff --git a/lib/core/models/card-view-dateitem.model.ts b/lib/core/card-view/models/card-view-dateitem.model.ts similarity index 66% rename from lib/core/models/card-view-dateitem.model.ts rename to lib/core/card-view/models/card-view-dateitem.model.ts index 2ec89e66ac..efd303cc4a 100644 --- a/lib/core/models/card-view-dateitem.model.ts +++ b/lib/core/card-view/models/card-view-dateitem.model.ts @@ -15,30 +15,23 @@ * limitations under the License. */ -/** - * - * This object represent the basic structure of a card view. - * - * - * @returns {CardViewDateItemModel} . - */ - import moment from 'moment-es6'; -import { CardViewItem } from '../interface/card-view-item.interface'; -import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; -import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; - -export interface CardViewDateItemProperties extends CardViewItemProperties { - format?: string; -} +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewBaseItemModel } from './card-view-baseitem.model'; +import { CardViewDateItemProperties } from '../interfaces/card-view.interfaces'; export class CardViewDateItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'date'; - format: string; + format: string = 'MMM DD YYYY'; constructor(obj: CardViewDateItemProperties) { super(obj); - this.format = obj.format || 'MMM DD YYYY'; + + if (obj.format) { + this.format = obj.format; + } + } get displayValue() { diff --git a/lib/core/card-view/models/card-view-datetimeitem.model.ts b/lib/core/card-view/models/card-view-datetimeitem.model.ts new file mode 100644 index 0000000000..bef5c813f3 --- /dev/null +++ b/lib/core/card-view/models/card-view-datetimeitem.model.ts @@ -0,0 +1,25 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewDateItemModel } from './card-view-dateitem.model'; + +export class CardViewDatetimeItemModel extends CardViewDateItemModel implements CardViewItem, DynamicComponentModel { + type: string = 'datetime'; + format: string = 'MMM DD YYYY HH:mm'; +} diff --git a/lib/core/card-view/models/card-view-floatitem.model.spec.ts b/lib/core/card-view/models/card-view-floatitem.model.spec.ts new file mode 100644 index 0000000000..7a47f6e6d6 --- /dev/null +++ b/lib/core/card-view/models/card-view-floatitem.model.spec.ts @@ -0,0 +1,59 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewFloatItemModel } from './card-view-floatitem.model'; +import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; + +describe('CardViewFloatItemModel', () => { + + let properties: CardViewTextItemProperties; + + beforeEach(() => { + properties = { + label: 'Tribe', + value: '42.42', + key: 'tribe' + }; + }); + + it('value should be parsed as float', () => { + const itemModel = new CardViewFloatItemModel(properties); + + expect(itemModel.value).toBe(42.42); + }); + + it('value should be parsed as float only if there is a value', () => { + properties.value = undefined; + const itemModel = new CardViewFloatItemModel(properties); + + expect(itemModel.value).toBe(undefined); + }); + + it('isValid should return the validator\'s value', () => { + const itemModel = new CardViewFloatItemModel(properties); + + expect(itemModel.isValid(42)).toBe(true, 'For 42 it should be true'); + expect(itemModel.isValid(42.0)).toBe(true, 'For 42.0 it should be true'); + expect(itemModel.isValid('42')).toBe(true, 'For "42" it should be true'); + expect(itemModel.isValid('42.0')).toBe(true, 'For "42.0" it should be true'); + expect(itemModel.isValid('4e2')).toBe(true, 'For "4e2" it should be true'); + expect(itemModel.isValid('4g2')).toBe(false, 'For "4g2" it should be false'); + expect(itemModel.isValid(42.3)).toBe(true, 'For 42.3 it should be true'); + expect(itemModel.isValid('42.3')).toBe(true, 'For "42.3" it should be true'); + expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false'); + }); +}); diff --git a/lib/core/card-view/models/card-view-floatitem.model.ts b/lib/core/card-view/models/card-view-floatitem.model.ts new file mode 100644 index 0000000000..a302f25a96 --- /dev/null +++ b/lib/core/card-view/models/card-view-floatitem.model.ts @@ -0,0 +1,35 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewTextItemModel } from './card-view-textitem.model'; +import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; +import { CardViewItemFloatValidator } from '..//validators/card-view.validators'; + +export class CardViewFloatItemModel extends CardViewTextItemModel implements CardViewItem, DynamicComponentModel { + type: string = 'float'; + + constructor(obj: CardViewTextItemProperties) { + super(obj); + + this.validators.push(new CardViewItemFloatValidator()); + if (obj.value) { + this.value = parseFloat(obj.value); + } + } +} diff --git a/lib/core/card-view/models/card-view-intitem.model.spec.ts b/lib/core/card-view/models/card-view-intitem.model.spec.ts new file mode 100644 index 0000000000..f6e7ee818a --- /dev/null +++ b/lib/core/card-view/models/card-view-intitem.model.spec.ts @@ -0,0 +1,59 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewIntItemModel } from './card-view-intitem.model'; +import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; + +describe('CardViewIntItemModel', () => { + + let properties: CardViewTextItemProperties; + + beforeEach(() => { + properties = { + label: 'Tribe', + value: '42', + key: 'tribe' + }; + }); + + it('value should be parsed as integer', () => { + const itemModel = new CardViewIntItemModel(properties); + + expect(itemModel.value).toBe(42); + }); + + it('value should be parsed as integer only if there is a value', () => { + properties.value = undefined; + const itemModel = new CardViewIntItemModel(properties); + + expect(itemModel.value).toBe(undefined); + }); + + it('isValid should return the validator\'s value', () => { + const itemModel = new CardViewIntItemModel(properties); + + expect(itemModel.isValid(42)).toBe(true, 'For 42 it should be true'); + expect(itemModel.isValid(42.0)).toBe(true, 'For 42.0 it should be true'); + expect(itemModel.isValid('42')).toBe(true, 'For "42" it should be true'); + expect(itemModel.isValid('42.0')).toBe(true, 'For "42.0" it should be true'); + expect(itemModel.isValid('4e2')).toBe(true, 'For "4e2" it should be true'); + expect(itemModel.isValid('4g2')).toBe(false, 'For "4g2" it should be false'); + expect(itemModel.isValid(42.3)).toBe(false, 'For 42.3 it should be false'); + expect(itemModel.isValid('42.3')).toBe(false, 'For "42.3" it should be false'); + expect(itemModel.isValid('test')).toBe(false, 'For "test" it should be false'); + }); +}); diff --git a/lib/core/card-view/models/card-view-intitem.model.ts b/lib/core/card-view/models/card-view-intitem.model.ts new file mode 100644 index 0000000000..09f38b6738 --- /dev/null +++ b/lib/core/card-view/models/card-view-intitem.model.ts @@ -0,0 +1,35 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewTextItemModel } from './card-view-textitem.model'; +import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; +import { CardViewItemIntValidator } from '../validators/card-view.validators'; + +export class CardViewIntItemModel extends CardViewTextItemModel implements CardViewItem, DynamicComponentModel { + type: string = 'int'; + + constructor(obj: CardViewTextItemProperties) { + super(obj); + + this.validators.push(new CardViewItemIntValidator()); + if (obj.value) { + this.value = parseInt(obj.value, 10); + } + } +} diff --git a/lib/core/models/card-view-mapitem.model.ts b/lib/core/card-view/models/card-view-mapitem.model.ts similarity index 69% rename from lib/core/models/card-view-mapitem.model.ts rename to lib/core/card-view/models/card-view-mapitem.model.ts index 61fd04085d..5f0f52111f 100644 --- a/lib/core/models/card-view-mapitem.model.ts +++ b/lib/core/card-view/models/card-view-mapitem.model.ts @@ -15,26 +15,14 @@ * limitations under the License. */ -/** - * - * This object represent the basic structure of a card view. - * - * - * @returns {CardViewMapItemModel} . - */ - -import { CardViewItem } from '../interface/card-view-item.interface'; -import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; -import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewBaseItemModel } from './card-view-baseitem.model'; export class CardViewMapItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'map'; value: Map; - constructor(obj: CardViewItemProperties) { - super(obj); - } - get displayValue() { if (this.value && this.value.size > 0) { return this.value.values().next().value; diff --git a/lib/core/models/card-view-textitem.model.spec.ts b/lib/core/card-view/models/card-view-textitem.model.spec.ts similarity index 63% rename from lib/core/models/card-view-textitem.model.spec.ts rename to lib/core/card-view/models/card-view-textitem.model.spec.ts index 240dc17803..2fa78b6db7 100644 --- a/lib/core/models/card-view-textitem.model.spec.ts +++ b/lib/core/card-view/models/card-view-textitem.model.spec.ts @@ -16,7 +16,8 @@ */ import { PipeTransform } from '@angular/core'; -import { CardViewTextItemModel, CardViewTextItemProperties } from './card-view-textitem.model'; +import { CardViewTextItemModel } from './card-view-textitem.model'; +import { CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; class TestPipe implements PipeTransform { transform(value: string, pipeParam: string): string { @@ -39,28 +40,36 @@ describe('CardViewTextItemModel', () => { describe('displayValue', () => { - it('should return the extension if file has it', () => { - const file = new CardViewTextItemModel(properties); + it('should return the value if it is present', () => { + const itemModel = new CardViewTextItemModel(properties); - expect(file.displayValue).toBe('Banuk'); + expect(itemModel.displayValue).toBe('Banuk'); + }); + + it('should return the default value if the value is not present', () => { + properties.value = undefined; + properties.default = 'default-value'; + const itemModel = new CardViewTextItemModel(properties); + + expect(itemModel.displayValue).toBe('default-value'); }); it('should apply a pipe on the value if it is present', () => { properties.pipes = [ { pipe: new TestPipe() } ]; - const file = new CardViewTextItemModel(properties); + const itemModel = new CardViewTextItemModel(properties); - expect(file.displayValue).toBe('testpiped-Banuk'); + expect(itemModel.displayValue).toBe('testpiped-Banuk'); }); it('should apply a pipe on the value with parameters if those are present', () => { properties.pipes = [ { pipe: new TestPipe(), params: ['withParams'] } ]; - const file = new CardViewTextItemModel(properties); + const itemModel = new CardViewTextItemModel(properties); - expect(file.displayValue).toBe('testpiped-Banuk-withParams'); + expect(itemModel.displayValue).toBe('testpiped-Banuk-withParams'); }); it('should apply more pipes on the value with parameters if those are present', () => { @@ -70,9 +79,9 @@ describe('CardViewTextItemModel', () => { { pipe, params: ['2'] }, { pipe, params: ['3'] } ]; - const file = new CardViewTextItemModel(properties); + const itemModel = new CardViewTextItemModel(properties); - expect(file.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3'); + expect(itemModel.displayValue).toBe('testpiped-testpiped-testpiped-Banuk-1-2-3'); }); }); }); diff --git a/lib/core/models/card-view-textitem.model.ts b/lib/core/card-view/models/card-view-textitem.model.ts similarity index 62% rename from lib/core/models/card-view-textitem.model.ts rename to lib/core/card-view/models/card-view-textitem.model.ts index 48eea977dc..a30dfcabbe 100644 --- a/lib/core/models/card-view-textitem.model.ts +++ b/lib/core/card-view/models/card-view-textitem.model.ts @@ -15,31 +15,15 @@ * limitations under the License. */ -/** - * - * This object represent the basic structure of a card view. - * - * - * @returns {CardViewTextItemModel} . - */ +import { CardViewItem } from '../interfaces/card-view-item.interface'; +import { DynamicComponentModel } from '../../services/dynamic-component-mapper.service'; +import { CardViewBaseItemModel } from './card-view-baseitem.model'; +import { CardViewTextItemPipeProperty, CardViewTextItemProperties } from '../interfaces/card-view.interfaces'; -import { PipeTransform } from '@angular/core'; -import { CardViewItem } from '../interface/card-view-item.interface'; -import { DynamicComponentModel } from '../services/dynamic-component-mapper.service'; -import { CardViewBaseItemModel, CardViewItemProperties } from './card-view-baseitem.model'; - -export interface CardViewTextItemPipeProperty { - pipe: PipeTransform; - params?: Array; -} -export interface CardViewTextItemProperties extends CardViewItemProperties { - multiline?: boolean; - pipes?: Array; -} export class CardViewTextItemModel extends CardViewBaseItemModel implements CardViewItem, DynamicComponentModel { type: string = 'text'; multiline?: boolean; - pipes?: Array; + pipes?: CardViewTextItemPipeProperty[]; constructor(obj: CardViewTextItemProperties) { super(obj); @@ -48,7 +32,11 @@ export class CardViewTextItemModel extends CardViewBaseItemModel implements Card } get displayValue() { - return this.applyPipes(this.value); + if (this.isEmpty()) { + return this.default; + } else { + return this.applyPipes(this.value); + } } private applyPipes(displayValue) { diff --git a/lib/core/card-view/models/card-view.models.ts b/lib/core/card-view/models/card-view.models.ts new file mode 100644 index 0000000000..0d16dc6552 --- /dev/null +++ b/lib/core/card-view/models/card-view.models.ts @@ -0,0 +1,25 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './card-view-baseitem.model'; +export * from './card-view-boolitem.model'; +export * from './card-view-dateitem.model'; +export * from './card-view-datetimeitem.model'; +export * from './card-view-floatitem.model'; +export * from './card-view-intitem.model'; +export * from './card-view-mapitem.model'; +export * from './card-view-textitem.model'; diff --git a/lib/core/card-view/public-api.ts b/lib/core/card-view/public-api.ts index 14c9afc436..6d6b8d8bc0 100644 --- a/lib/core/card-view/public-api.ts +++ b/lib/core/card-view/public-api.ts @@ -15,9 +15,16 @@ * limitations under the License. */ -export * from './card-view-content-proxy.directive'; -export * from './card-view-dateitem.component'; -export * from './card-view-item-dispatcher.component'; -export * from './card-view-mapitem.component'; -export * from './card-view-textitem.component'; -export * from './card-view.component'; +export { + CardViewComponent, + CardViewBoolItemComponent, + CardViewDateItemComponent, + CardViewMapItemComponent, + CardViewTextItemComponent +} from './components/card-view.components'; + +export * from './interfaces/card-view.interfaces'; +export * from './validators/card-view.validators'; +export * from './models/card-view.models'; +export * from './services/card-view.services'; +export * from './directives/card-view-content-proxy.directive'; diff --git a/lib/core/services/card-item-types.service.ts b/lib/core/card-view/services/card-item-types.service.ts similarity index 59% rename from lib/core/services/card-item-types.service.ts rename to lib/core/card-view/services/card-item-types.service.ts index 2b7d7e1866..e237d44005 100644 --- a/lib/core/services/card-item-types.service.ts +++ b/lib/core/card-view/services/card-item-types.service.ts @@ -16,10 +16,11 @@ */ import { Injectable, Type } from '@angular/core'; -import { CardViewDateItemComponent } from '../card-view/card-view-dateitem.component'; -import { CardViewMapItemComponent } from '../card-view/card-view-mapitem.component'; -import { CardViewTextItemComponent } from '../card-view/card-view-textitem.component'; -import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from '../services/dynamic-component-mapper.service'; +import { CardViewDateItemComponent } from '../components/card-view-dateitem/card-view-dateitem.component'; +import { CardViewMapItemComponent } from '../components/card-view-mapitem/card-view-mapitem.component'; +import { CardViewTextItemComponent } from '../components/card-view-textitem/card-view-textitem.component'; +import { CardViewBoolItemComponent } from '../components/card-view-boolitem/card-view-boolitem.component'; +import { DynamicComponentMapper, DynamicComponentResolveFunction, DynamicComponentResolver } from '../../services/dynamic-component-mapper.service'; @Injectable() export class CardItemTypeService extends DynamicComponentMapper { @@ -28,7 +29,11 @@ export class CardItemTypeService extends DynamicComponentMapper { protected types: { [key: string]: DynamicComponentResolveFunction } = { 'text': DynamicComponentResolver.fromType(CardViewTextItemComponent), + 'int': DynamicComponentResolver.fromType(CardViewTextItemComponent), + 'float': DynamicComponentResolver.fromType(CardViewTextItemComponent), 'date': DynamicComponentResolver.fromType(CardViewDateItemComponent), + 'datetime': DynamicComponentResolver.fromType(CardViewDateItemComponent), + 'bool': DynamicComponentResolver.fromType(CardViewBoolItemComponent), 'map': DynamicComponentResolver.fromType(CardViewMapItemComponent) }; } diff --git a/lib/core/services/card-view-update.service.spec.ts b/lib/core/card-view/services/card-view-update.service.spec.ts similarity index 100% rename from lib/core/services/card-view-update.service.spec.ts rename to lib/core/card-view/services/card-view-update.service.spec.ts diff --git a/lib/core/services/card-view-update.service.ts b/lib/core/card-view/services/card-view-update.service.ts similarity index 100% rename from lib/core/services/card-view-update.service.ts rename to lib/core/card-view/services/card-view-update.service.ts diff --git a/lib/core/card-view/services/card-view.services.ts b/lib/core/card-view/services/card-view.services.ts new file mode 100644 index 0000000000..a64e2fa0b5 --- /dev/null +++ b/lib/core/card-view/services/card-view.services.ts @@ -0,0 +1,19 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './card-item-types.service'; +export * from './card-view-update.service'; diff --git a/lib/core/card-view/validators/card-view-item-float.validator.ts b/lib/core/card-view/validators/card-view-item-float.validator.ts new file mode 100644 index 0000000000..e1fe97ffa5 --- /dev/null +++ b/lib/core/card-view/validators/card-view-item-float.validator.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemValidator } from '../interfaces/card-view.interfaces'; + +export class CardViewItemFloatValidator implements CardViewItemValidator { + + message = 'CORE.CARDVIEW.VALIDATORS.FLOAT_VALIDATION_ERROR'; + + isValid(value: any): boolean { + return !isNaN(parseFloat(value)) && isFinite(value); + } +} diff --git a/lib/core/card-view/validators/card-view-item-int.validator.ts b/lib/core/card-view/validators/card-view-item-int.validator.ts new file mode 100644 index 0000000000..962611551e --- /dev/null +++ b/lib/core/card-view/validators/card-view-item-int.validator.ts @@ -0,0 +1,27 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CardViewItemValidator } from '../interfaces/card-view.interfaces'; + +export class CardViewItemIntValidator implements CardViewItemValidator { + + message = 'CORE.CARDVIEW.VALIDATORS.INT_VALIDATION_ERROR'; + + isValid(value: any): boolean { + return !isNaN(value) && (function(x) { return (x | 0) === x; })(parseFloat(value)); + } +} diff --git a/lib/core/card-view/validators/card-view.validators.ts b/lib/core/card-view/validators/card-view.validators.ts new file mode 100644 index 0000000000..a924d5da02 --- /dev/null +++ b/lib/core/card-view/validators/card-view.validators.ts @@ -0,0 +1,19 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './card-view-item-int.validator'; +export * from './card-view-item-float.validator'; diff --git a/lib/core/collapsable/accordion-group.component.ts b/lib/core/collapsable/accordion-group.component.ts index c4b98a9bfb..939b29516b 100644 --- a/lib/core/collapsable/accordion-group.component.ts +++ b/lib/core/collapsable/accordion-group.component.ts @@ -31,21 +31,27 @@ export class AccordionGroupComponent implements OnDestroy { @ViewChild('contentWrapper') contentWrapper: any; + /** Title heading for the group. */ @Input() heading: string; + /** The material design icon. */ @Input() headingIcon: string; + /** Tooltip message to be shown for headingIcon */ @Input() headingIconTooltip: string; + /** Should the (expanded) accordion icon be shown? */ @Input() hasAccordionIcon: boolean = true; + /** Emitted when the heading is clicked. */ @Output() headingClick: EventEmitter = new EventEmitter(); + /** Is this group currently open? */ @Input() set isOpen(value: boolean) { this._isOpen = value; @@ -58,6 +64,7 @@ export class AccordionGroupComponent implements OnDestroy { return this._isOpen; } + /** Is this group currently selected? */ @Input() set isSelected(value: boolean) { this._isSelected = value; diff --git a/lib/core/context-menu/context-menu.directive.ts b/lib/core/context-menu/context-menu.directive.ts index 75e2ca9042..ff5cb13f3c 100644 --- a/lib/core/context-menu/context-menu.directive.ts +++ b/lib/core/context-menu/context-menu.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, HostListener, Input } from '@angular/core'; import { ContextMenuService } from './context-menu.service'; diff --git a/lib/core/core.module.ts b/lib/core/core.module.ts index f55d0e1796..012c43d73c 100644 --- a/lib/core/core.module.ts +++ b/lib/core/core.module.ts @@ -41,6 +41,7 @@ import { ToolbarModule } from './toolbar/toolbar.module'; import { UserInfoModule } from './userinfo/userinfo.module'; import { ViewerModule } from './viewer/viewer.module'; import { FormModule } from './form/form.module'; +import { SideBarActionModule } from './sidebar/sidebar-action.module'; import { DirectiveModule } from './directives/directive.module'; import { PipeModule } from './pipes/pipe.module'; @@ -55,6 +56,7 @@ export function createTranslateLoader(http: HttpClient, logService: LogService) @NgModule({ imports: [ ViewerModule, + SideBarActionModule, PipeModule, CommonModule, DirectiveModule, @@ -113,6 +115,7 @@ export function createTranslateLoader(http: HttpClient, logService: LogService) HostSettingsModule, ServiceModule, ViewerModule, + SideBarActionModule, PipeModule, DirectiveModule, FormModule, diff --git a/lib/core/data-column/data-column.component.ts b/lib/core/data-column/data-column.component.ts index 175f387bc4..938bf1db9b 100644 --- a/lib/core/data-column/data-column.component.ts +++ b/lib/core/data-column/data-column.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-input-rename */ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core'; @@ -25,33 +25,44 @@ import { Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/co }) export class DataColumnComponent implements OnInit { + /** Data source key. Can be either a column/property key like `title` + * or a property path like `createdBy.name`. + */ @Input() key: string; + /** Value type for the column. Possible settings are 'text', 'image', + * 'date', 'fileSize' and 'location'. + */ @Input() type: string = 'text'; + /** Value format (if supported by the parent component), for example format of the date. */ @Input() format: string; + /** Toggles ability to sort by this column, for example by clicking the column header. */ @Input() sortable: boolean = true; + /** Display title of the column, typically used for column headers. You can use the + * i18n resource key to get it translated automatically. + */ @Input() title: string = ''; @ContentChild(TemplateRef) template: any; + /** Custom tooltip formatter function. */ @Input() formatTooltip: Function; - /** - * Title to be used for screen readers. - */ + /** Title to be used for screen readers. */ @Input('sr-title') srTitle: string; + /** Additional CSS class to be applied to column (header and cells). */ @Input('class') cssClass: string; diff --git a/lib/core/datatable/components/datatable/datatable.component.scss b/lib/core/datatable/components/datatable/datatable.component.scss index db7424270b..861858c4a8 100644 --- a/lib/core/datatable/components/datatable/datatable.component.scss +++ b/lib/core/datatable/components/datatable/datatable.component.scss @@ -120,6 +120,7 @@ color: $data-table-header-color; padding-bottom: 8px; box-sizing: border-box; + white-space: nowrap; &.sortable { @include no-select; diff --git a/lib/core/datatable/components/datatable/datatable.component.ts b/lib/core/datatable/components/datatable/datatable.component.ts index 993cd0ef35..50560ffe61 100644 --- a/lib/core/datatable/components/datatable/datatable.component.ts +++ b/lib/core/datatable/components/datatable/datatable.component.ts @@ -49,57 +49,82 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck @ContentChild(DataColumnListComponent) columnList: DataColumnListComponent; + /* Data source for the table */ @Input() data: DataTableAdapter; + /* The rows that the datatable will show */ @Input() rows: any[] = []; + /* Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, + * you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. + */ @Input() selectionMode: string = 'single'; // none|single|multiple + /* Toggles multiple row selection, which renders checkboxes at the beginning of each row */ @Input() multiselect: boolean = false; + /* Toggles data actions column */ @Input() actions: boolean = false; + /* Position of the actions dropdown menu. Can be "left" or "right". */ @Input() actionsPosition: string = 'right'; // left|right + /* Fallback image for row where thumbnail is missing */ @Input() fallbackThumbnail: string; + /* Toggles custom context menu for the component */ @Input() contextMenu: boolean = false; + /* Toggle file drop support for rows (see Upload Directive for further details) */ @Input() allowDropFiles: boolean = false; + /* The inline style to apply to every row. See + * [NgStyle](https://angular.io/docs/ts/latest/api/common/index/NgStyle-directive.html) + * docs for more details and usage examples. + */ @Input() rowStyle: string; + /* The CSS class to apply to every row */ @Input() rowStyleClass: string = ''; + /* Toggles the header */ @Input() showHeader: boolean = true; + /* Emitted when user clicks the row */ @Output() rowClick = new EventEmitter(); + /* Emitted when user double-clicks the row */ @Output() rowDblClick = new EventEmitter(); + /* Emitted before context menu is displayed for a row */ @Output() showRowContextMenu = new EventEmitter(); + /* Emitted before actions menu is displayed for a row */ @Output() showRowActionsMenu = new EventEmitter(); + /* Emitted when row action is executed by user */ @Output() executeRowAction = new EventEmitter(); + /* Flag that indicates if the datatable is in loading state and needs to show the + * loading template (see the docs to learn how to configure a loading template). + */ @Input() loading: boolean = false; diff --git a/lib/core/datatable/components/datatable/filesize-cell.component.ts b/lib/core/datatable/components/datatable/filesize-cell.component.ts index 4413ace72e..350be11fe9 100644 --- a/lib/core/datatable/components/datatable/filesize-cell.component.ts +++ b/lib/core/datatable/components/datatable/filesize-cell.component.ts @@ -15,12 +15,11 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; +import { Component, ViewEncapsulation } from '@angular/core'; import { DataTableCellComponent } from './datatable-cell.component'; @Component({ selector: 'adf-filesize-cell', - changeDetection: ChangeDetectionStrategy.OnPush, template: ` {{ value | adfFileSize }} diff --git a/lib/core/directives/highlight.directive.ts b/lib/core/directives/highlight.directive.ts index 78a6db810c..875a87ae9e 100644 --- a/lib/core/directives/highlight.directive.ts +++ b/lib/core/directives/highlight.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, ElementRef, Input, Renderer } from '@angular/core'; import { HighlightTransformService, HightlightTransformResult } from '../services/highlight-transform.service'; diff --git a/lib/core/directives/node-delete.directive.ts b/lib/core/directives/node-delete.directive.ts index aa6b281ca4..92522be416 100644 --- a/lib/core/directives/node-delete.directive.ts +++ b/lib/core/directives/node-delete.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output } from '@angular/core'; import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; diff --git a/lib/core/directives/node-favorite.directive.ts b/lib/core/directives/node-favorite.directive.ts index 6044899f0b..bbebf938c7 100644 --- a/lib/core/directives/node-favorite.directive.ts +++ b/lib/core/directives/node-favorite.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, EventEmitter, HostListener, Input, OnChanges, Output } from '@angular/core'; import { FavoriteBody, MinimalNodeEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; diff --git a/lib/core/directives/node-permission.directive.ts b/lib/core/directives/node-permission.directive.ts index 0df5b49e87..9370d1d43b 100644 --- a/lib/core/directives/node-permission.directive.ts +++ b/lib/core/directives/node-permission.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { ChangeDetectorRef, Directive, ElementRef, Host, Inject, Input, OnChanges, Optional, Renderer2, SimpleChanges } from '@angular/core'; import { MinimalNodeEntity } from 'alfresco-js-api'; import { ContentService } from './../services/content.service'; diff --git a/lib/core/directives/node-restore.directive.ts b/lib/core/directives/node-restore.directive.ts index 2815233237..68839b7deb 100644 --- a/lib/core/directives/node-restore.directive.ts +++ b/lib/core/directives/node-restore.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:component-selector no-input-rename */ + import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core'; import { Router } from '@angular/router'; import { DeletedNodeEntry, DeletedNodesPaging, PathInfoEntity } from 'alfresco-js-api'; diff --git a/lib/core/directives/upload.directive.ts b/lib/core/directives/upload.directive.ts index 35b25b1028..a0800e34cb 100644 --- a/lib/core/directives/upload.directive.ts +++ b/lib/core/directives/upload.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { Directive, ElementRef, HostListener, Input, NgZone, OnDestroy, OnInit, Renderer } from '@angular/core'; import { FileInfo, FileUtils } from '../utils/file-utils'; diff --git a/lib/core/form/components/form-field/form-field.component.ts b/lib/core/form/components/form-field/form-field.component.ts index fce62d13a8..3184697250 100644 --- a/lib/core/form/components/form-field/form-field.component.ts +++ b/lib/core/form/components/form-field/form-field.component.ts @@ -68,6 +68,10 @@ export class FormFieldComponent implements OnInit, OnDestroy { } ngOnInit() { + const w: any = window; + if (w.adf === undefined) { + w.adf = {}; + } let originalField = this.getField(); if (originalField) { let customTemplate = this.field.form.customFieldTemplates[originalField.type]; diff --git a/lib/core/form/components/form.component.html b/lib/core/form/components/form.component.html index 7eb25440fe..19edbb04a6 100644 --- a/lib/core/form/components/form.component.html +++ b/lib/core/form/components/form.component.html @@ -2,7 +2,7 @@
-
+
@@ -12,8 +12,7 @@ refresh
- {{ form.isValid ? 'event_available' : 'event_busy' }} - {{form.taskName}} + {{form.taskName}} @@ -29,9 +28,10 @@
- + - - -
-
- - -

Select content

-
-
- -
- diff --git a/lib/core/form/components/widgets/attach/attach.widget.spec.ts b/lib/core/form/components/widgets/attach/attach.widget.spec.ts deleted file mode 100644 index d1fb09e366..0000000000 --- a/lib/core/form/components/widgets/attach/attach.widget.spec.ts +++ /dev/null @@ -1,302 +0,0 @@ -/*! - * @license - * Copyright 2016 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ - -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Observable } from 'rxjs/Observable'; -import { ActivitiContentService } from '../../../services/activiti-alfresco.service'; -import { MaterialModule } from '../../../../material.module'; -import { ExternalContent } from '../core/external-content'; -import { ExternalContentLink } from '../core/external-content-link'; -import { FormFieldTypes } from '../core/form-field-types'; -import { ErrorWidgetComponent } from '../error/error.component'; -import { EcmModelService } from './../../../services/ecm-model.service'; -import { FormService } from './../../../services/form.service'; -import { FormFieldModel } from './../core/form-field.model'; -import { FormModel } from './../core/form.model'; -import { AttachWidgetComponent } from './attach.widget'; - -describe('AttachWidgetComponent', () => { - - let widget: AttachWidgetComponent; - let fixture: ComponentFixture; - let contentService: ActivitiContentService; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - MaterialModule - ], - declarations: [ - AttachWidgetComponent, - ErrorWidgetComponent - ], - providers: [ - FormService, - EcmModelService, - ActivitiContentService - ] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AttachWidgetComponent); - contentService = TestBed.get(ActivitiContentService); - - widget = fixture.componentInstance; - }); - - it('should require field value to check file', () => { - widget.field = null; - widget.ngOnInit(); - expect(widget.hasFile()).toBeFalsy(); - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - value: null - }); - widget.ngOnInit(); - expect(widget.hasFile()).toBeFalsy(); - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - value: [{ name: 'file' }] - }); - widget.ngOnInit(); - expect(widget.hasFile()).toBeTruthy(); - }); - - it('should setup with form field', () => { - let nodes: any = [{}]; - spyOn(contentService, 'getAlfrescoNodes').and.returnValue( - Observable.create(observer => { - observer.next(nodes); - observer.complete(); - }) - ); - - let config = { - siteId: '', - site: '', - pathId: '', - accountId: '' - }; - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - params: { - fileSource: { - selectedFolder: config - } - } - }); - widget.ngOnInit(); - - expect(widget.selectedFolderSiteId).toBe(config.siteId); - expect(widget.selectedFolderSiteName).toBe(config.site); - expect(widget.selectedFolderPathId).toBe(config.pathId); - expect(widget.selectedFolderAccountId).toBe(config.accountId); - expect(widget.selectedFolderNodes).toEqual(nodes); - }); - - xit('should link file on select', () => { - let link = {}; - spyOn(contentService, 'linkAlfrescoNode').and.returnValue( - Observable.create(observer => { - observer.next(link); - observer.complete(); - }) - ); - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD - }); - widget.ngOnInit(); - - let node = {}; - widget.selectFile(node, null); - - expect(contentService.linkAlfrescoNode).toHaveBeenCalled(); - expect(widget.selectedFile).toBe(node); - expect(widget.field.value).toEqual([link]); - expect(widget.field.json.value).toEqual([link]); - expect(widget.hasFile()).toBeTruthy(); - }); - - it('should reset', () => { - widget.field = new FormFieldModel(new FormModel(), { - type: FormFieldTypes.UPLOAD, - value: [{ name: 'filename' }] - }); - - widget.reset(); - expect(widget.hasFile()).toBeFalsy(); - expect(widget.field.value).toBeNull(); - expect(widget.field.json.value).toBeNull(); - expect(widget.hasFile()).toBeFalsy(); - }); - - it('should close dialog on cancel', () => { - let closed = false; - widget.dialog = { - nativeElement: { - close: function () { - closed = true; - } - } - }; - widget.cancel(); - expect(closed).toBeTruthy(); - }); - - xit('should show modal dialog', () => { - spyOn(contentService, 'getAlfrescoNodes').and.returnValue( - Observable.create(observer => { - observer.next([]); - observer.complete(); - }) - ); - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - params: { - fileSource: { - selectedFolder: {} - } - } - }); - - let modalShown = false; - widget.dialog = { - nativeElement: { - showModal: function () { - modalShown = true; - } - } - }; - - widget.showDialog(); - expect(modalShown).toBeTruthy(); - }); - - it('should select folder and load nodes', () => { - let nodes: any = [{}]; - spyOn(contentService, 'getAlfrescoNodes').and.returnValue( - Observable.create(observer => { - observer.next(nodes); - observer.complete(); - }) - ); - - let node = { id: '' }; - widget.selectFolder(node, null); - - expect(widget.selectedFolderPathId).toBe(node.id); - expect(widget.selectedFolderNodes).toEqual(nodes); - }); - - it('should get linked file name via local variable', () => { - widget.fileName = ''; - widget.selectedFile = null; - widget.field = null; - expect(widget.getLinkedFileName()).toBe(widget.fileName); - }); - - it('should get linked file name via selected file', () => { - widget.fileName = null; - widget.selectedFile = { title: '' }; - widget.field = null; - expect(widget.getLinkedFileName()).toBe(widget.selectedFile.title); - }); - - it('should get linked file name via form field', () => { - widget.fileName = null; - widget.selectedFile = null; - - let name = '<file>'; - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - value: [{ name: name }] - }); - - expect(widget.getLinkedFileName()).toBe(name); - }); - - it('should require form field to setup file browser', () => { - widget.field = null; - widget.setupFileBrowser(); - - expect(widget.selectedFolderPathId).toBeUndefined(); - expect(widget.selectedFolderAccountId).toBeUndefined(); - - const pathId = '<pathId>'; - const accountId = '<accountId>'; - - widget.field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - params: { - fileSource: { - selectedFolder: { - pathId: pathId, - accountId: accountId - } - } - } - }); - widget.setupFileBrowser(); - expect(widget.selectedFolderPathId).toBe(pathId); - expect(widget.selectedFolderAccountId).toBe(accountId); - }); - - it('should get external content nodes', () => { - let nodes: any = [{}]; - spyOn(contentService, 'getAlfrescoNodes').and.returnValue( - Observable.create(observer => { - observer.next(nodes); - observer.complete(); - }) - ); - - const accountId = '<accountId>'; - const pathId = '<pathId>'; - widget.selectedFolderAccountId = accountId; - widget.selectedFolderPathId = pathId; - widget.getExternalContentNodes(); - - expect(contentService.getAlfrescoNodes).toHaveBeenCalledWith(accountId, pathId); - expect(widget.selectedFolderNodes).toEqual(nodes); - }); - - it('should handle error', (done) => { - let error = 'error'; - spyOn(contentService, 'getAlfrescoNodes').and.returnValue( - Observable.throw(error) - ); - - widget.error.subscribe(() => { - done(); - }); - - widget.getExternalContentNodes(); - }); - - it('should require configured dialog to show modal', () => { - widget.dialog = null; - spyOn(widget, 'setupFileBrowser').and.stub(); - spyOn(widget, 'getExternalContentNodes').and.stub(); - expect(widget.showDialog()).toBeFalsy(); - }); -}); diff --git a/lib/core/form/components/widgets/attach/attach.widget.ts b/lib/core/form/components/widgets/attach/attach.widget.ts deleted file mode 100644 index 088a3c764c..0000000000 --- a/lib/core/form/components/widgets/attach/attach.widget.ts +++ /dev/null @@ -1,156 +0,0 @@ -/*! - * @license - * Copyright 2016 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ - - /* tslint:disable:component-selector */ - -import { Component, EventEmitter, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core'; -import { ActivitiContentService } from '../../../services/activiti-alfresco.service'; -import { ExternalContent } from '../core/external-content'; -import { ExternalContentLink } from '../core/external-content-link'; -import { FormFieldModel } from '../core/form-field.model'; -import { FormService } from './../../../services/form.service'; -import { baseHost , WidgetComponent } from './../widget.component'; - -@Component({ - selector: 'attach-widget', - templateUrl: './attach.widget.html', - styleUrls: ['./attach.widget.css'], - host: baseHost, - encapsulation: ViewEncapsulation.None -}) -export class AttachWidgetComponent extends WidgetComponent implements OnInit { - - selectedFolderPathId: string; - selectedFolderSiteId: string; - selectedFolderSiteName: string; - selectedFolderAccountId: string; - fileName: string; - selectedFolderNodes: [ExternalContent]; - selectedFile: ExternalContent; - - @Output() - fieldChanged: EventEmitter<FormFieldModel> = new EventEmitter<FormFieldModel>(); - - @Output() - error: EventEmitter<any> = new EventEmitter<any>(); - - @ViewChild('dialog') - dialog: any; - - constructor(public formService: FormService, - private contentService: ActivitiContentService) { - super(formService); - } - - ngOnInit() { - if (this.field) { - let params = this.field.params; - - if (params && - params.fileSource && - params.fileSource.selectedFolder) { - this.selectedFolderSiteId = params.fileSource.selectedFolder.siteId; - this.selectedFolderSiteName = params.fileSource.selectedFolder.site; - this.setupFileBrowser(); - this.getExternalContentNodes(); - } - } - } - - setupFileBrowser() { - if (this.field) { - let params = this.field.params; - this.selectedFolderPathId = params.fileSource.selectedFolder.pathId; - this.selectedFolderAccountId = params.fileSource.selectedFolder.accountId; - } - } - - getLinkedFileName(): string { - let result = this.fileName; - - if (this.selectedFile && - this.selectedFile.title) { - result = this.selectedFile.title; - } - if (this.field && - this.field.value && - this.field.value.length > 0 && - this.field.value[0].name) { - result = this.field.value[0].name; - } - - return result; - } - - getExternalContentNodes() { - this.contentService.getAlfrescoNodes(this.selectedFolderAccountId, this.selectedFolderPathId) - .subscribe( - nodes => this.selectedFolderNodes = nodes, - (err) => { - this.error.emit(err); - } - ); - } - - selectFile(node: ExternalContent, $event: any) { - this.contentService.linkAlfrescoNode(this.selectedFolderAccountId, node, this.selectedFolderSiteId).subscribe( - (link: ExternalContentLink) => { - this.selectedFile = node; - this.field.value = [link]; - this.field.json.value = [link]; - this.closeDialog(); - this.fieldChanged.emit(this.field); - } - ); - } - - selectFolder(node: ExternalContent, $event: any) { - this.selectedFolderPathId = node.id; - this.getExternalContentNodes(); - } - - showDialog(): boolean { - this.setupFileBrowser(); - this.getExternalContentNodes(); - - if (this.dialog) { - // todo: show dialog - return true; - } - return false; - } - - private closeDialog() { - if (this.dialog) { - this.dialog.nativeElement.close(); - } - } - - cancel() { - this.closeDialog(); - } - - reset() { - this.field.value = null; - this.field.json.value = null; - } - - hasFile(): boolean { - return this.field && this.field.value; - } - -} diff --git a/lib/core/form/components/widgets/checkbox/checkbox.widget.ts b/lib/core/form/components/widgets/checkbox/checkbox.widget.ts index ad517c3513..a4ccf77b95 100644 --- a/lib/core/form/components/widgets/checkbox/checkbox.widget.ts +++ b/lib/core/form/components/widgets/checkbox/checkbox.widget.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -/* tslint:disable:component-selector */ +/* tslint:disable:component-selector no-input-rename */ import { Component, ViewEncapsulation } from '@angular/core'; import { WidgetVisibilityService } from '../../../services/widget-visibility.service'; diff --git a/lib/core/form/components/widgets/core/index.ts b/lib/core/form/components/widgets/core/index.ts index 4cd936b504..daab3ca5ba 100644 --- a/lib/core/form/components/widgets/core/index.ts +++ b/lib/core/form/components/widgets/core/index.ts @@ -33,3 +33,5 @@ export * from './form-outcome-event.model'; export * from './form-field-validator'; export * from './content-link.model'; export * from './error-message.model'; +export * from './external-content'; +export * from './external-content-link'; diff --git a/lib/core/form/components/widgets/dropdown/dropdown.widget.spec.ts b/lib/core/form/components/widgets/dropdown/dropdown.widget.spec.ts index af675290c7..8fb63854bb 100644 --- a/lib/core/form/components/widgets/dropdown/dropdown.widget.spec.ts +++ b/lib/core/form/components/widgets/dropdown/dropdown.widget.spec.ts @@ -304,7 +304,7 @@ describe('DropdownWidgetComponent', () => { fixture.detectChanges(); expect(element.querySelector('#dropdown-id')).not.toBeNull(); const option = fixture.debugElement.query(By.css('.mat-option')).nativeElement; - expect(option.innerText).toEqual('FakeValue'); + expect(option.innerText.trim()).toEqual('FakeValue'); }); })); }); diff --git a/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.scss b/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.scss index 03c629d693..bc3f4779ff 100644 --- a/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.scss +++ b/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.scss @@ -19,6 +19,11 @@ $dynamic-table-cell-top: $dynamic-table-card-padding / 2; $dynamic-table-drag-border: 1px dashed rgb(68, 138, 255); + dynamic-table-widget .adf-label { + width: auto; + height: auto; + } + .adf { &-dynamic-table { diff --git a/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.spec.ts b/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.spec.ts index 5d2768492c..511af7a664 100644 --- a/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.spec.ts +++ b/lib/core/form/components/widgets/dynamic-table/dynamic-table.widget.spec.ts @@ -359,7 +359,7 @@ describe('DynamicTableWidgetComponent', () => { expect(element.querySelector('#dynamic-table-fake-dynamic-table')).not.toBeNull(); expect(rowElement).not.toBeNull(); - expect(rowElement.className).toBeFalsy(); + expect(rowElement.className).not.toContain('adf-dynamic-table-widget__row-selected'); let event: any = new Event('keyup'); event.keyCode = 32; @@ -368,7 +368,7 @@ describe('DynamicTableWidgetComponent', () => { fixture.whenStable().then(() => { let selectedRow = element.querySelector('#fake-dynamic-table-row-0'); - expect(selectedRow.className).toBe('adf-dynamic-table-widget__row-selected'); + expect(selectedRow.className).toContain('adf-dynamic-table-widget__row-selected'); }); })); diff --git a/lib/core/form/components/widgets/index.ts b/lib/core/form/components/widgets/index.ts index d793a18cf5..3fa1231187 100644 --- a/lib/core/form/components/widgets/index.ts +++ b/lib/core/form/components/widgets/index.ts @@ -20,7 +20,6 @@ import { TabsWidgetComponent } from './tabs/tabs.widget'; import { UnknownWidgetComponent } from './unknown/unknown.widget'; import { AmountWidgetComponent } from './amount/amount.widget'; -import { AttachWidgetComponent } from './attach/attach.widget'; import { CheckboxWidgetComponent } from './checkbox/checkbox.widget'; import { DateWidgetComponent } from './date/date.widget'; import { DisplayTextWidgetComponentComponent } from './display-text/display-text.widget'; @@ -63,7 +62,6 @@ export * from './hyperlink/hyperlink.widget'; export * from './radio-buttons/radio-buttons.widget'; export * from './display-text/display-text.widget'; export * from './upload/upload.widget'; -export * from './attach/attach.widget'; export * from './typeahead/typeahead.widget'; export * from './functional-group/functional-group.widget'; export * from './people/people.widget'; @@ -95,7 +93,6 @@ export const WIDGET_DIRECTIVES: any[] = [ RadioButtonsWidgetComponent, DisplayTextWidgetComponentComponent, UploadWidgetComponent, - AttachWidgetComponent, TypeaheadWidgetComponent, FunctionalGroupWidgetComponent, PeopleWidgetComponent, diff --git a/lib/core/form/components/widgets/number/number.widget.ts b/lib/core/form/components/widgets/number/number.widget.ts index 513fd1833e..476f182328 100644 --- a/lib/core/form/components/widgets/number/number.widget.ts +++ b/lib/core/form/components/widgets/number/number.widget.ts @@ -15,7 +15,7 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-input-rename */ import { Component, ViewEncapsulation } from '@angular/core'; import { FormService } from './../../../services/form.service'; diff --git a/lib/core/form/components/widgets/text/text-mask.component.ts b/lib/core/form/components/widgets/text/text-mask.component.ts index 98474b9be1..c53f33f78e 100644 --- a/lib/core/form/components/widgets/text/text-mask.component.ts +++ b/lib/core/form/components/widgets/text/text-mask.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ - /* tslint:disable: component-selector, no-use-before-declare */ + /* tslint:disable: component-selector no-use-before-declare no-input-rename */ import { Directive, diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html new file mode 100644 index 0000000000..abe6ce2694 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.html @@ -0,0 +1,7 @@ +<div class="adf-upload-folder-widget {{field.className}}" + [class.adf-invalid]="!field.isValid" + [class.adf-readonly]="field.readOnly"> +<label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label> + <div class="adf-upload-widget-container"> + </div> +</div> diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss new file mode 100644 index 0000000000..2dddb9ee90 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.scss @@ -0,0 +1,12 @@ +@import '../form'; + + +.adf { + + &-upload-folder-widget { + width: 100%; + word-break: break-all; + padding: 0.4375em 0; + border-top: 0.84375em solid transparent; + } +} diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts new file mode 100644 index 0000000000..58ad8b656b --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.spec.ts @@ -0,0 +1,16 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ diff --git a/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts new file mode 100644 index 0000000000..31df392ea2 --- /dev/null +++ b/lib/core/form/components/widgets/upload-folder/upload-folder.widget.ts @@ -0,0 +1,150 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/* tslint:disable:component-selector */ + +import { LogService } from '../../../../services/log.service'; +import { ThumbnailService } from '../../../../services/thumbnail.service'; +import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { FormService } from '../../../services/form.service'; +import { ProcessContentService } from '../../../services/process-content.service'; +import { ContentLinkModel } from '../core/content-link.model'; +import { baseHost, WidgetComponent } from './../widget.component'; +import 'rxjs/add/operator/mergeMap'; + +@Component({ + selector: 'upload-folder-widget', + templateUrl: './upload-folder.widget.html', + styleUrls: ['./upload-folder.widget.scss'], + host: baseHost, + encapsulation: ViewEncapsulation.None +}) +export class UploadFolderWidgetComponent extends WidgetComponent implements OnInit { + + hasFile: boolean; + displayText: string; + multipleOption: string = ''; + mimeTypeIcon: string; + + @ViewChild('uploadFiles') + fileInput: ElementRef; + + constructor(public formService: FormService, + private logService: LogService, + private thumbnailService: ThumbnailService, + public processContentService: ProcessContentService) { + super(formService); + } + + ngOnInit() { + if (this.field && + this.field.value && + this.field.value.length > 0) { + this.hasFile = true; + } + this.getMultipleFileParam(); + } + + removeFile(file: any) { + if (this.field) { + this.removeElementFromList(file); + } + } + + onFileChanged(event: any) { + let files = event.target.files; + let filesSaved = []; + + if (this.field.json.value) { + filesSaved = [...this.field.json.value]; + } + + if (files && files.length > 0) { + Observable.from(files).mergeMap(file => this.uploadRawContent(file)).subscribe((res) => { + filesSaved.push(res); + }, + (error) => { + this.logService.error('Error uploading file. See console output for more details.'); + }, + () => { + this.field.value = filesSaved; + this.field.json.value = filesSaved; + }); + + this.hasFile = true; + } + } + + private uploadRawContent(file): Observable<any> { + return this.processContentService.createTemporaryRawRelatedContent(file) + .map((response: any) => { + this.logService.info(response); + return response; + }); + } + + private getMultipleFileParam() { + if (this.field && + this.field.params && + this.field.params.multiple) { + this.multipleOption = this.field.params.multiple ? 'multiple' : ''; + } + } + + private removeElementFromList(file) { + let index = this.field.value.indexOf(file); + + if (index !== -1) { + this.field.value.splice(index, 1); + this.field.json.value = this.field.value; + this.field.updateForm(); + } + + this.hasFile = this.field.value.length > 0; + + this.resetFormValueWithNoFiles(); + } + + private resetFormValueWithNoFiles() { + if (this.field.value.length === 0) { + this.field.value = []; + this.field.json.value = []; + } + } + + getIcon(mimeType) { + return this.thumbnailService.getMimeTypeIcon(mimeType); + } + + fileClicked(obj: any): void { + const file = new ContentLinkModel(obj); + let fetch = this.processContentService.getContentPreview(file.id); + if (file.isTypeImage() || file.isTypePdf()) { + fetch = this.processContentService.getFileRawContent(file.id); + } + fetch.subscribe( + (blob: Blob) => { + file.contentBlob = blob; + this.formService.formContentClicked.next(file); + }, + (error) => { + this.logService.error('Unable to send event for file ' + file.name); + } + ); + } +} diff --git a/lib/core/form/components/widgets/upload/upload.widget.ts b/lib/core/form/components/widgets/upload/upload.widget.ts index fc4d195087..c6a50dee7f 100644 --- a/lib/core/form/components/widgets/upload/upload.widget.ts +++ b/lib/core/form/components/widgets/upload/upload.widget.ts @@ -98,7 +98,7 @@ export class UploadWidgetComponent extends WidgetComponent implements OnInit { }); } - private getMultipleFileParam() { + getMultipleFileParam() { if (this.field && this.field.params && this.field.params.multiple) { diff --git a/lib/core/form/public-api.ts b/lib/core/form/public-api.ts index 5fa6d1cddf..bfc50da1cc 100644 --- a/lib/core/form/public-api.ts +++ b/lib/core/form/public-api.ts @@ -27,5 +27,6 @@ export * from './services/ecm-model.service'; export * from './services/node.service'; export * from './services/form-rendering.service'; export * from './services/process-content.service'; +export * from './services/activiti-alfresco.service'; export * from './events/index'; diff --git a/lib/core/form/services/activiti-alfresco.service.ts b/lib/core/form/services/activiti-alfresco.service.ts index 4b1ae760bb..574bffbb83 100644 --- a/lib/core/form/services/activiti-alfresco.service.ts +++ b/lib/core/form/services/activiti-alfresco.service.ts @@ -18,7 +18,7 @@ import { AlfrescoApiService } from '../../services/alfresco-api.service'; import { LogService } from '../../services/log.service'; import { Injectable } from '@angular/core'; -import { AlfrescoApi } from 'alfresco-js-api'; +import { AlfrescoApi, MinimalNodeEntryEntity } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; import { ExternalContent } from '../components/widgets/core/external-content'; import { ExternalContentLink } from '../components/widgets/core/external-content-link'; @@ -49,6 +49,24 @@ export class ActivitiContentService { .catch(err => this.handleError(err)); } + /** + * Returns a list of all the repositories configured + * + * @param accountId + * @param folderId + * @returns {any} + */ + getAlfrescoRepositories(tenantId: number, includeAccount: boolean): Observable<any> { + let apiService: AlfrescoApi = this.apiService.getInstance(); + const opts = { + tenantId: tenantId, + includeAccounts: includeAccount + }; + return Observable.fromPromise(apiService.activiti.alfrescoApi.getRepositories(opts)) + .map(this.toJsonArray) + .catch(err => this.handleError(err)); + } + /** * Returns a list of child nodes below the specified folder * @@ -68,6 +86,20 @@ export class ActivitiContentService { })).map(this.toJson).catch(err => this.handleError(err)); } + applyAlfrescoNode(node: MinimalNodeEntryEntity, siteId: string, accountId: string) { + let apiService: AlfrescoApi = this.apiService.getInstance(); + let params: any = { + source: accountId, + sourceId: node.id + ';1.0@' + siteId, + name: node.name, + link: false + }; + return Observable.fromPromise( + apiService.activiti.contentApi.createTemporaryRelatedContent(params)) + .map(this.toJson) + .catch(err => this.handleError(err)); + } + toJson(res: any) { if (res) { return res || {}; diff --git a/lib/core/form/services/form-rendering.service.spec.ts b/lib/core/form/services/form-rendering.service.spec.ts index eafb18e735..5f0a65ea4b 100644 --- a/lib/core/form/services/form-rendering.service.spec.ts +++ b/lib/core/form/services/form-rendering.service.spec.ts @@ -17,7 +17,6 @@ import { DynamicComponentResolver } from '../../index'; import { - AttachWidgetComponent, FormFieldModel, FormFieldTypes, UnknownWidgetComponent, @@ -33,17 +32,6 @@ describe('FormRenderingService', () => { service = new FormRenderingService(); }); - it('should resolve Upload field as Attach widget', () => { - let field = new FormFieldModel(null, { - type: FormFieldTypes.UPLOAD, - params: { - link: 'link' - } - }); - let type = service.resolveComponentType(field); - expect(type).toBe(AttachWidgetComponent); - }); - it('should resolve Upload field as Upload widget', () => { let field = new FormFieldModel(null, { type: FormFieldTypes.UPLOAD, @@ -55,10 +43,10 @@ describe('FormRenderingService', () => { expect(type).toBe(UploadWidgetComponent); }); - it('should resolve Unknown widget for Upload field', () => { + it('should resolve Upload widget for Upload field', () => { let resolver = service.getComponentTypeResolver(FormFieldTypes.UPLOAD); let type = resolver(null); - expect(type).toBe(UnknownWidgetComponent); + expect(type).toBe(UploadWidgetComponent); }); it('should resolve Uknown widget for unknown field type', () => { @@ -67,12 +55,6 @@ describe('FormRenderingService', () => { expect(type).toBe(UnknownWidgetComponent); }); - it('shoulld resolve custom value for unknown field type', () => { - let resolver = service.getComponentTypeResolver('missing-type', AttachWidgetComponent); - let type = resolver(null); - expect(type).toBe(AttachWidgetComponent); - }); - it('should fallback to default resolver when field type missing', () => { let resolver = service.getComponentTypeResolver(null); let type = resolver(null); @@ -123,7 +105,7 @@ describe('FormRenderingService', () => { }); it('should return custom value when resolving with no field', () => { - expect(service.resolveComponentType(null, AttachWidgetComponent)).toBe(AttachWidgetComponent); + expect(service.resolveComponentType(null, UploadWidgetComponent)).toBe(UploadWidgetComponent); }); }); diff --git a/lib/core/form/services/form-rendering.service.ts b/lib/core/form/services/form-rendering.service.ts index 45c8346ea7..6ad1082e19 100644 --- a/lib/core/form/services/form-rendering.service.ts +++ b/lib/core/form/services/form-rendering.service.ts @@ -20,7 +20,6 @@ import { Injectable, Type } from '@angular/core'; import { AmountWidgetComponent, - AttachWidgetComponent, CheckboxWidgetComponent, ContainerWidgetComponent, DateWidgetComponent, @@ -28,7 +27,6 @@ import { DocumentWidgetComponent, DropdownWidgetComponent, DynamicTableWidgetComponent, - FormFieldModel, FunctionalGroupWidgetComponent, HyperlinkWidgetComponent, MultilineTextWidgetComponentComponent, @@ -63,21 +61,11 @@ export class FormRenderingService extends DynamicComponentMapper { 'dynamic-table': DynamicComponentResolver.fromType(DynamicTableWidgetComponent), 'container': DynamicComponentResolver.fromType(ContainerWidgetComponent), 'group': DynamicComponentResolver.fromType(ContainerWidgetComponent), - 'document': DynamicComponentResolver.fromType(DocumentWidgetComponent) + 'document': DynamicComponentResolver.fromType(DocumentWidgetComponent), + 'upload': DynamicComponentResolver.fromType(UploadWidgetComponent) }; constructor() { super(); - - this.types['upload'] = (field: FormFieldModel): Type<{}> => { - if (field) { - let params = field.params; - if (params && params.link) { - return AttachWidgetComponent; - } - return UploadWidgetComponent; - } - return UnknownWidgetComponent; - }; } } diff --git a/lib/core/i18n/de.json b/lib/core/i18n/de.json index bca5c09460..38490c45d0 100644 --- a/lib/core/i18n/de.json +++ b/lib/core/i18n/de.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Startformular" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Vorschau nicht verfügbar" + }, + "FIELD": { + "LOCALSTORAGE": "Lokaler Speicherort", + "SOURCE": "Quelle auswählen aus ", + "UPLOAD": "Hochladen", + "REQUIRED": "*Erforderlich", + "VALIDATOR": { + "INVALID_NUMBER": "Verwenden Sie ein anderes Zahlenformat", + "INVALID_DATE": "Verwenden Sie ein anderes Datumsformat", + "INVALID_VALUE": "Geben Sie einen anderen Wert ein", + "NOT_GREATER_THAN": "Darf nicht größer als {{ maxValue }} sein", + "NOT_LESS_THAN": "Darf nicht kleiner als {{ minValue }} sein", + "AT_LEAST_LONG": "Geben Sie mindestens {{ minLength }} Zeichen ein", + "NO_LONGER_THAN": "Geben Sie höchstens {{ maxLength }} Zeichen ein" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "Bytes", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Angezeigt werden {{ range }} von {{ total }}", "ITEMS_PER_PAGE": "Elemente pro Seite", @@ -71,6 +105,27 @@ "BACK": "Zurück", "APPLY": "Anwenden", "NOT_VALID": "http(s)://host|ip:port(/path) nicht erkannt. Probieren Sie es mit einer anderen URL." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Verwenden Sie ein Zahlenformat", + "INT_VALIDATION_ERROR": "Verwenden Sie ein Ganzzahlenformat" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Eigenschaften", + "NAME": "Name", + "TITLE": "Titel", + "DESCRIPTION": "Beschreibung", + "AUTHOR": "Autor", + "MIMETYPE": "MIME-Type", + "SIZE": "Größe", + "CREATOR": "Ersteller", + "CREATED_DATE": "Datum der Erstellung", + "MODIFIER": "Bearbeiter", + "MODIFIED_DATE": "Datum der Änderung" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "von" }, "LOADING": "Laden", - "UNKNOWN_FORMAT": "Vorschau konnte nicht geladen werden" + "UNKNOWN_FORMAT": "Vorschau konnte nicht geladen werden", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Mehr Informationen", + "LESS_INFORMATION": "Weniger Informationen" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index 7cd077d03f..9b5f07b361 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Start Form" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Preview not available" + }, + "FIELD": { + "LOCALSTORAGE" : "Local storage", + "SOURCE": "Select source from ", + "UPLOAD": "UPLOAD", + "REQUIRED": "*Required", + "VALIDATOR": { + "INVALID_NUMBER": "Use a different number format", + "INVALID_DATE": "Use a different date format", + "INVALID_VALUE": "Enter a different value", + "NOT_GREATER_THAN": "Can't be greater than {{ maxValue }}", + "NOT_LESS_THAN": "Can't be less than {{ minValue }}", + "AT_LEAST_LONG": "Enter at least {{ minLength }} characters", + "NO_LONGER_THAN": "Enter no more than {{ maxLength }} characters" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "Bytes", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Showing {{ range }} of {{ total }}", "ITEMS_PER_PAGE": "Items per page", @@ -72,8 +106,15 @@ "APPLY": "APPLY", "NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL." }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Use a number format", + "INT_VALIDATION_ERROR": "Use an integer format" + } + }, "METADATA": { "BASIC": { + "HEADER": "Properties", "NAME": "Name", "TITLE": "Title", "DESCRIPTION": "Description", diff --git a/lib/core/i18n/es.json b/lib/core/i18n/es.json index 56b348ef19..c8d1045409 100644 --- a/lib/core/i18n/es.json +++ b/lib/core/i18n/es.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Iniciar formulario" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Vista previa no disponible" + }, + "FIELD": { + "LOCALSTORAGE": "Almacenamiento local", + "SOURCE": "Seleccionar origen de ", + "UPLOAD": "Cargar", + "REQUIRED": "*Obligatorio", + "VALIDATOR": { + "INVALID_NUMBER": "Utilizar un formato de número diferente", + "INVALID_DATE": "Utilizar un formato de fecha diferente", + "INVALID_VALUE": "Introducir un valor diferente", + "NOT_GREATER_THAN": "No puede ser mayor que {{ maxValue }}", + "NOT_LESS_THAN": "No puede ser menor que {{ minValue }}", + "AT_LEAST_LONG": "Introducir al menos {{ minLength }} caracteres", + "NO_LONGER_THAN": "No introducir más de {{ maxLength }} caracteres" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "bytes", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Mostrando {{ range }} de {{ total }}", "ITEMS_PER_PAGE": "Elementos por página", @@ -71,6 +105,27 @@ "BACK": "Volver", "APPLY": "Aplicar", "NOT_VALID": "http(s)://host|ip:port(/path) no reconocido; pruebe con una URL diferente." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Utilizar un formato de número", + "INT_VALIDATION_ERROR": "Utilizar un formato de entero" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Propiedades", + "NAME": "Nombre", + "TITLE": "Título", + "DESCRIPTION": "Descripción", + "AUTHOR": "Autor", + "MIMETYPE": "Tipo MIME", + "SIZE": "Tamaño", + "CREATOR": "Creador", + "CREATED_DATE": "Fecha de creación", + "MODIFIER": "Modificador", + "MODIFIED_DATE": "Fecha de modificación" + } } }, "LOGIN": { @@ -89,7 +144,7 @@ "LOGIN-ERROR-PROVIDERS": "Los proveedores no pueden estar no definidos", "LOGIN-ERROR-CORS": "Excepción CORS, compruebe la configuración del servidor", "LOGIN-ERROR-CSRF": "Excepción CSRF, configure [disableCsrf]=\"true\" en login.component", - "LOGIN-ECM-LICENSE": "El respositorio de Alfresco Content Services está en modo solo lectura" + "LOGIN-ECM-LICENSE": "El repositorio de Alfresco Content Services está en modo solo lectura" }, "BUTTON": { "LOGIN": "Iniciar sesión", @@ -146,6 +201,12 @@ "OF": "de" }, "LOADING": "Cargando", - "UNKNOWN_FORMAT": "No se ha podido cargar la vista previa" + "UNKNOWN_FORMAT": "No se ha podido cargar la vista previa", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Más información", + "LESS_INFORMATION": "Menos información" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/fr.json b/lib/core/i18n/fr.json index 4bcfd68a24..a7055f8bee 100644 --- a/lib/core/i18n/fr.json +++ b/lib/core/i18n/fr.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Démarrer le formulaire" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Aperçu non disponible" + }, + "FIELD": { + "LOCALSTORAGE": "Stockage local", + "SOURCE": "Sélectionner la source dans ", + "UPLOAD": "Importer", + "REQUIRED": "*Obligatoire", + "VALIDATOR": { + "INVALID_NUMBER": "Utiliser un format de numéro différent", + "INVALID_DATE": "Utiliser un format de date différent", + "INVALID_VALUE": "Saisir une valeur différente", + "NOT_GREATER_THAN": "Ne peut pas être supérieur à {{ maxValue }}", + "NOT_LESS_THAN": "Ne peut pas être inférieur à {{ minValue }}", + "AT_LEAST_LONG": "Saisir {{ minLength }} caractères minimum", + "NO_LONGER_THAN": "Saisir {{ maxLength }} caractères maximum" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "Octets", + "KB": "Ko", + "MB": "Mo", + "GB": "Go", + "TB": "To", + "PB": "Po", + "EB": "Eo", + "ZB": "Zo", + "YB": "Yo" + }, "PAGINATION": { "ITEMS_RANGE": "Affichage de {{ range }} sur {{ total }}", "ITEMS_PER_PAGE": "Éléments par page", @@ -71,6 +105,27 @@ "BACK": "Retour", "APPLY": "Appliquer", "NOT_VALID": "Adresse http(s)://host|ip:port(/path) non reconnue, essayez une autre URL." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Utiliser un format de numéro", + "INT_VALIDATION_ERROR": "Utiliser un format d'entier" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Propriétés", + "NAME": "Nom", + "TITLE": "Titre", + "DESCRIPTION": "Description", + "AUTHOR": "Auteur", + "MIMETYPE": "Type MIME", + "SIZE": "Taille", + "CREATOR": "Créateur", + "CREATED_DATE": "Date de création", + "MODIFIER": "Modificateur", + "MODIFIED_DATE": "Date de Modification" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "sur" }, "LOADING": "Chargement en cours", - "UNKNOWN_FORMAT": "Impossible de charger l'aperçu" + "UNKNOWN_FORMAT": "Impossible de charger l'aperçu", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Plus d'informations", + "LESS_INFORMATION": "Moins d'informations" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/it.json b/lib/core/i18n/it.json index b4ca34cb72..341f4f7b58 100644 --- a/lib/core/i18n/it.json +++ b/lib/core/i18n/it.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Modulo di inizio" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Anteprima non disponibile" + }, + "FIELD": { + "LOCALSTORAGE": "Archivio locale", + "SOURCE": "Seleziona origine da ", + "UPLOAD": "Carica", + "REQUIRED": "*Obbligatorio", + "VALIDATOR": { + "INVALID_NUMBER": "Utilizzare un formato numerico diverso", + "INVALID_DATE": "Utilizzare un formato data diverso", + "INVALID_VALUE": "Immettere un altro valore", + "NOT_GREATER_THAN": "Non può essere maggiore di {{ maxValue }}", + "NOT_LESS_THAN": "Non può essere minore di {{ minValue }}", + "AT_LEAST_LONG": "Immettere almeno {{ minLength }} caratteri", + "NO_LONGER_THAN": "Immettere non più di {{ maxLength }} caratteri" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "byte", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Visualizzazione di {{ range }} su {{ total }}", "ITEMS_PER_PAGE": "Elementi per pagina", @@ -71,6 +105,27 @@ "BACK": "Indietro", "APPLY": "Applica", "NOT_VALID": "http(s)://host|ip:port(/path) non riconosciuto, provare un URL diverso." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Utilizzare un formato numerico", + "INT_VALIDATION_ERROR": "Utilizzare numeri interi" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Proprietà", + "NAME": "Nome", + "TITLE": "Titolo", + "DESCRIPTION": "Descrizione", + "AUTHOR": "Autore", + "MIMETYPE": "Tipo MIME", + "SIZE": "Dimensione", + "CREATOR": "Creatore", + "CREATED_DATE": "Data di creazione", + "MODIFIER": "Modificatore", + "MODIFIED_DATE": "Data di modifica" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "di" }, "LOADING": "Caricamento in corso", - "UNKNOWN_FORMAT": "Impossibile caricare l'anteprima" + "UNKNOWN_FORMAT": "Impossibile caricare l'anteprima", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Più informazioni", + "LESS_INFORMATION": "Meno informazioni" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/ja.json b/lib/core/i18n/ja.json index 480b9abd8d..d5e3e074c3 100644 --- a/lib/core/i18n/ja.json +++ b/lib/core/i18n/ja.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "開始フォーム" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "プレビューは使用できません" + }, + "FIELD": { + "LOCALSTORAGE": "ローカルストレージ", + "SOURCE": "ソースの選択先 ", + "UPLOAD": "アップロード", + "REQUIRED": "*必須", + "VALIDATOR": { + "INVALID_NUMBER": "別の番号形式を使用してください", + "INVALID_DATE": "別の日付形式を使用してください", + "INVALID_VALUE": "別の値を入力してください", + "NOT_GREATER_THAN": "{{ maxValue }} より大きい値にすることはできません", + "NOT_LESS_THAN": "{{ minValue }} より小さい値にすることはできません", + "AT_LEAST_LONG": "{{ minLength }} 文字以上で入力してください", + "NO_LONGER_THAN": "{{ maxLength }} 文字以内で入力してください" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "バイト", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "表示中: {{ range }} / {{ total }} 件", "ITEMS_PER_PAGE": "1 ページのアイテム数", @@ -71,6 +105,27 @@ "BACK": "戻る", "APPLY": "適用", "NOT_VALID": "http(s)://host|ip:port(/path) が認識されません。別の URL を指定してください." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "番号形式を使用してください", + "INT_VALIDATION_ERROR": "整数形式を使用してください" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "プロパティ", + "NAME": "名前", + "TITLE": "タイトル", + "DESCRIPTION": "説明", + "AUTHOR": "作成者", + "MIMETYPE": "MIME タイプ", + "SIZE": "サイズ", + "CREATOR": "作成者", + "CREATED_DATE": "作成日", + "MODIFIER": "変更者", + "MODIFIED_DATE": "変更日" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "/" }, "LOADING": "読み込み中", - "UNKNOWN_FORMAT": "プレビューを読み込めませんでした" + "UNKNOWN_FORMAT": "プレビューを読み込めませんでした", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "詳細", + "LESS_INFORMATION": "概要" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/nb.json b/lib/core/i18n/nb.json index d5d1e31d63..dd2f8b3937 100644 --- a/lib/core/i18n/nb.json +++ b/lib/core/i18n/nb.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Start skjema" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Forhåndsvisning er ikke tilgjengelig" + }, + "FIELD": { + "LOCALSTORAGE": "Lokal lagring", + "SOURCE": "Velg kilde fra ", + "UPLOAD": "Last opp", + "REQUIRED": "*Påkrevd", + "VALIDATOR": { + "INVALID_NUMBER": "Bruk et annet nummerformat", + "INVALID_DATE": "Bruk et annet datoformat", + "INVALID_VALUE": "Angi en annen verdi", + "NOT_GREATER_THAN": "Kan ikke være større enn {{ maxValue }}", + "NOT_LESS_THAN": "Kan ikke være mindre enn {{ minValue }}", + "AT_LEAST_LONG": "Angi minst {{ minLength }} tegn", + "NO_LONGER_THAN": "Angi opptil {{ maxLength }} tegn" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "Byte", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Vis {{ range }} av {{ total }}", "ITEMS_PER_PAGE": "Elementer per side", @@ -71,6 +105,27 @@ "BACK": "Tilbake", "APPLY": "Bruk", "NOT_VALID": "http(s)://host|ip:port(/path) ikke gjenkjent, prøv en annen URL." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Bruk et nummerformat", + "INT_VALIDATION_ERROR": "Bruk heltall" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Egenskaper", + "NAME": "Navn", + "TITLE": "Tittel", + "DESCRIPTION": "Beskrivelse", + "AUTHOR": "Forfatter", + "MIMETYPE": "Mimetype", + "SIZE": "Størrelse", + "CREATOR": "Oppretter", + "CREATED_DATE": "Opprettelsesdato", + "MODIFIER": "Modifikator", + "MODIFIED_DATE": "Endringsdato" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "av" }, "LOADING": "Laster", - "UNKNOWN_FORMAT": "Kan ikke laste forhåndsvisning" + "UNKNOWN_FORMAT": "Kan ikke laste forhåndsvisning", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Mer informasjon", + "LESS_INFORMATION": "Mindre informasjon" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/nl.json b/lib/core/i18n/nl.json index c7dd3185ae..fe4c196149 100644 --- a/lib/core/i18n/nl.json +++ b/lib/core/i18n/nl.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Beginformulier" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Voorbeeld niet beschikbaar" + }, + "FIELD": { + "LOCALSTORAGE": "Lokale opslag", + "SOURCE": "Selecteer bron uit ", + "UPLOAD": "Uploaden", + "REQUIRED": "*Vereist", + "VALIDATOR": { + "INVALID_NUMBER": "Gebruik een andere getalnotatie", + "INVALID_DATE": "Gebruik een andere datumnotatie", + "INVALID_VALUE": "Voer een andere waarde in", + "NOT_GREATER_THAN": "Mag niet groter zijn dan {{ maxValue }}", + "NOT_LESS_THAN": "Mag niet kleiner zijn dan {{ minValue }}", + "AT_LEAST_LONG": "Voer ten minste {{ minLength }} tekens in", + "NO_LONGER_THAN": "Voer niet meer dan {{ maxLength }} tekens in" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "bytes", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "{{ range }} van {{ total }} wordt weergegeven", "ITEMS_PER_PAGE": "Items per pagina", @@ -71,6 +105,27 @@ "BACK": "Terug", "APPLY": "Toepassen", "NOT_VALID": "http(s)://host|ip:port(/path) niet herkend, probeer een andere URL." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Gebruik een getalnotatie", + "INT_VALIDATION_ERROR": "Gebruik een notatie voor gehele getallen" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Eigenschappen", + "NAME": "Naam", + "TITLE": "Titel", + "DESCRIPTION": "Beschrijving", + "AUTHOR": "Auteur", + "MIMETYPE": "Mimetype", + "SIZE": "Grootte", + "CREATOR": "Maker", + "CREATED_DATE": "Datum gemaakt", + "MODIFIER": "Gewijzigd door", + "MODIFIED_DATE": "Datum gewijzigd" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "van" }, "LOADING": "Wordt geladen", - "UNKNOWN_FORMAT": "Kan voorbeeld niet laden" + "UNKNOWN_FORMAT": "Kan voorbeeld niet laden", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Meer informatie", + "LESS_INFORMATION": "Minder informatie" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/pt-BR.json b/lib/core/i18n/pt-BR.json index 1ca23ecef3..fe6c56c215 100644 --- a/lib/core/i18n/pt-BR.json +++ b/lib/core/i18n/pt-BR.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Iniciar formulário" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Visualização não disponível" + }, + "FIELD": { + "LOCALSTORAGE": "Armazenamento local", + "SOURCE": "Selecionar origem de ", + "UPLOAD": "Carregar", + "REQUIRED": "*Obrigatório", + "VALIDATOR": { + "INVALID_NUMBER": "Use um formato de número diferente", + "INVALID_DATE": "Use um formato de data diferente", + "INVALID_VALUE": "Insira um valor diferente", + "NOT_GREATER_THAN": "Não pode ser maior do que {{ maxValue }}", + "NOT_LESS_THAN": "Não pode ser menor do que {{ minValue }}", + "AT_LEAST_LONG": "Insira no mínimo {{ minLength }} caracteres", + "NO_LONGER_THAN": "Insira no máximo {{ maxLength }} caracteres" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "Bytes", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "Mostrando {{ range }} de {{ total }}", "ITEMS_PER_PAGE": "Itens por página", @@ -71,6 +105,27 @@ "BACK": "Voltar", "APPLY": "Aplicar", "NOT_VALID": "http(s)://host|ip:port(/path) não reconhecida, tente uma URL diferente." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Use um formato de número", + "INT_VALIDATION_ERROR": "Use um formato inteiro" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Propriedades", + "NAME": "Nome", + "TITLE": "Título", + "DESCRIPTION": "Descrição", + "AUTHOR": "Autor", + "MIMETYPE": "Tipo mime", + "SIZE": "Tamanho", + "CREATOR": "Criador", + "CREATED_DATE": "Data de criação", + "MODIFIER": "Modificador", + "MODIFIED_DATE": "Data de modificação" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "de" }, "LOADING": "Carregando", - "UNKNOWN_FORMAT": "Não foi possível carregar pré-visualização" + "UNKNOWN_FORMAT": "Não foi possível carregar pré-visualização", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Mais informação", + "LESS_INFORMATION": "Menos informação" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/ru.json b/lib/core/i18n/ru.json index 1de39c030a..e636b8e6b9 100644 --- a/lib/core/i18n/ru.json +++ b/lib/core/i18n/ru.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "Запустить форму" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "Предварительный просмотр недоступен" + }, + "FIELD": { + "LOCALSTORAGE": "Локальное хранение", + "SOURCE": "Выберите источник из ", + "UPLOAD": "Загрузить", + "REQUIRED": "*Обязательное поле", + "VALIDATOR": { + "INVALID_NUMBER": "Используйте другой формат числа", + "INVALID_DATE": "Используйте другой формат даты", + "INVALID_VALUE": "Введите другое значение", + "NOT_GREATER_THAN": "Не может превышать {{ maxValue }}", + "NOT_LESS_THAN": "Не может быть меньше {{ minValue }}", + "AT_LEAST_LONG": "Введите не менее {{ minLength }} символов", + "NO_LONGER_THAN": "Введите не более {{ maxLength }} символов" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "б", + "KB": "КБ", + "MB": "МБ", + "GB": "ГБ", + "TB": "ТБ", + "PB": "ПБ", + "EB": "ЭБ", + "ZB": "ЗБ", + "YB": "ИБ" + }, "PAGINATION": { "ITEMS_RANGE": "Отображается {{ range }} из {{ total }}", "ITEMS_PER_PAGE": "Элементов на странице", @@ -71,6 +105,27 @@ "BACK": "Назад", "APPLY": "Применить", "NOT_VALID": "http(s)://host|ip:port(/path) не распознан, используйте другой URL-адрес." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "Использовать формат числа", + "INT_VALIDATION_ERROR": "Использовать формат целого числа" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "Свойства", + "NAME": "Имя", + "TITLE": "Заголовок", + "DESCRIPTION": "Описание", + "AUTHOR": "Автор", + "MIMETYPE": "Тип MIME", + "SIZE": "Размер", + "CREATOR": "Создатель", + "CREATED_DATE": "Дата создания", + "MODIFIER": "Редактор", + "MODIFIED_DATE": "Дата изменения" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "из" }, "LOADING": "Загружается", - "UNKNOWN_FORMAT": "Не удалось загрузить предварительный просмотр" + "UNKNOWN_FORMAT": "Не удалось загрузить предварительный просмотр", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "Дополнительные сведения", + "LESS_INFORMATION": "Краткие сведения" + } + } } } \ No newline at end of file diff --git a/lib/core/i18n/zh-CN.json b/lib/core/i18n/zh-CN.json index 21b1e449a4..bd5e9ebb31 100644 --- a/lib/core/i18n/zh-CN.json +++ b/lib/core/i18n/zh-CN.json @@ -1,5 +1,39 @@ { + "FORM": { + "START_FORM": { + "TITLE": "启动表单" + }, + "PREVIEW": { + "IMAGE_NOT_AVAILABLE": "预览不可用" + }, + "FIELD": { + "LOCALSTORAGE": "本地存储", + "SOURCE": "来源选自 ", + "UPLOAD": "上传", + "REQUIRED": "*必需", + "VALIDATOR": { + "INVALID_NUMBER": "使用其他数字格式", + "INVALID_DATE": "使用其他日期格式", + "INVALID_VALUE": "使用其他值", + "NOT_GREATER_THAN": "不能大于 {{ maxValue }}", + "NOT_LESS_THAN": "不能小于 {{ minValue }}", + "AT_LEAST_LONG": "至少输入 {{ minLength }} 个字符", + "NO_LONGER_THAN": "输入不超过 {{ maxLength }} 个字符" + } + } + }, "CORE": { + "FILE_SIZE": { + "BYTES": "字节", + "KB": "KB", + "MB": "MB", + "GB": "GB", + "TB": "TB", + "PB": "PB", + "EB": "EB", + "ZB": "ZB", + "YB": "YB" + }, "PAGINATION": { "ITEMS_RANGE": "显示 {{range}}/{{total}}", "ITEMS_PER_PAGE": "每页的项目", @@ -71,6 +105,27 @@ "BACK": "返回", "APPLY": "应用", "NOT_VALID": "http(s)://host|ip:port(/path) 无法识别,请使用其他 URL." + }, + "CARDVIEW": { + "VALIDATORS": { + "FLOAT_VALIDATION_ERROR": "使用数字格式", + "INT_VALIDATION_ERROR": "使用整数格式" + } + }, + "METADATA": { + "BASIC": { + "HEADER": "属性", + "NAME": "名称", + "TITLE": "标题", + "DESCRIPTION": "说明", + "AUTHOR": "作者", + "MIMETYPE": "MIME 类型", + "SIZE": "字号(&S)", + "CREATOR": "创建者", + "CREATED_DATE": "创建日期", + "MODIFIER": "修改者", + "MODIFIED_DATE": "修改日期" + } } }, "LOGIN": { @@ -146,6 +201,12 @@ "OF": "/" }, "LOADING": "正在加载", - "UNKNOWN_FORMAT": "无法加载预览" + "UNKNOWN_FORMAT": "无法加载预览", + "SIDEBAR": { + "METADATA": { + "MORE_INFORMATION": "更多信息", + "LESS_INFORMATION": "更少信息" + } + } } } \ No newline at end of file diff --git a/lib/core/index.ts b/lib/core/index.ts index 574b4cff70..6347462be1 100644 --- a/lib/core/index.ts +++ b/lib/core/index.ts @@ -45,6 +45,7 @@ export * from './pagination'; export * from './login'; export * from './language-menu'; export * from './info-drawer'; +export * from './sidebar'; export * from './data-column'; export * from './datatable'; export * from './context-menu'; diff --git a/lib/core/info-drawer/info-drawer.component.html b/lib/core/info-drawer/info-drawer.component.html index 8006c2fdd4..45b406728b 100644 --- a/lib/core/info-drawer/info-drawer.component.html +++ b/lib/core/info-drawer/info-drawer.component.html @@ -7,7 +7,7 @@ <ng-container info-drawer-content *ngIf="showTabLayout(); then tabLayout else singleLayout"></ng-container> <ng-template #tabLayout> - <mat-tab-group class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)"> + <mat-tab-group [(selectedIndex)]="selectedIndex" class="adf-info-drawer-tabs" (selectedTabChange)="onTabChange($event)"> <ng-container *ngFor="let contentBlock of contentBlocks"> <mat-tab [label]="contentBlock.label" class="adf-info-drawer-tab"> <ng-container *ngTemplateOutlet="contentBlock.content"></ng-container> @@ -19,4 +19,4 @@ <ng-template #singleLayout> <ng-content select="[info-drawer-content]"></ng-content> </ng-template> -</adf-info-drawer-layout> \ No newline at end of file +</adf-info-drawer-layout> diff --git a/lib/core/info-drawer/info-drawer.component.spec.ts b/lib/core/info-drawer/info-drawer.component.spec.ts index 8c3c594b93..4cd21fcd6d 100644 --- a/lib/core/info-drawer/info-drawer.component.spec.ts +++ b/lib/core/info-drawer/info-drawer.component.spec.ts @@ -22,6 +22,7 @@ import { By } from '@angular/platform-browser'; import { MaterialModule } from '../material.module'; import { InfoDrawerLayoutComponent } from './info-drawer-layout.component'; import { InfoDrawerComponent } from './info-drawer.component'; +import { InfoDrawerTabComponent } from './info-drawer.component'; describe('InfoDrawerComponent', () => { let element: HTMLElement; @@ -73,21 +74,28 @@ describe('InfoDrawerComponent', () => { @Component({ template: ` - <adf-info-drawer> + <adf-info-drawer [selectedIndex]="tabIndex"> <div info-drawer-title>Fake Title Custom</div> + <adf-info-drawer-tab label="Tab1"> + </adf-info-drawer-tab> + <adf-info-drawer-tab label="Tab2"> + </adf-info-drawer-tab> </adf-info-drawer> ` }) -class CustomInfoDrawerComponent { +class CustomInfoDrawerComponent extends InfoDrawerComponent { + tabIndex: number; } describe('Custom InfoDrawer', () => { let fixture: ComponentFixture<CustomInfoDrawerComponent>; + let component: CustomInfoDrawerComponent; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ InfoDrawerComponent, + InfoDrawerTabComponent, InfoDrawerLayoutComponent, CustomInfoDrawerComponent ], @@ -100,6 +108,7 @@ describe('Custom InfoDrawer', () => { beforeEach(() => { fixture = TestBed.createComponent(CustomInfoDrawerComponent); fixture.detectChanges(); + component = fixture.componentInstance; }); it('should render the title', () => { @@ -108,4 +117,19 @@ describe('Custom InfoDrawer', () => { expect(title.length).toBe(1); expect(title[0].nativeElement.innerText).toBe('Fake Title Custom'); }); + + it('should select the tab 1 (index 0) as default', () => { + fixture.detectChanges(); + let tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active')); + expect(tab.length).toBe(1); + expect(tab[0].nativeElement.innerText).toBe('Tab1'); + }); + + it('should select the tab 2 (index 1)', () => { + component.tabIndex = 1; + fixture.detectChanges(); + let tab: any = fixture.debugElement.queryAll(By.css('.mat-tab-label-active')); + expect(tab.length).toBe(1); + expect(tab[0].nativeElement.innerText).toBe('Tab2'); + }); }); diff --git a/lib/core/info-drawer/info-drawer.component.ts b/lib/core/info-drawer/info-drawer.component.ts index 4af5a8ab79..fa44ff6026 100644 --- a/lib/core/info-drawer/info-drawer.component.ts +++ b/lib/core/info-drawer/info-drawer.component.ts @@ -37,6 +37,9 @@ export class InfoDrawerComponent { @Input() title: string|null = null; + @Input() + selectedIndex: number = 0; + @Output() currentTab: EventEmitter<number> = new EventEmitter<number>(); diff --git a/lib/core/interface/public-api.ts b/lib/core/interface/public-api.ts index abc2977d14..b69f6f445a 100644 --- a/lib/core/interface/public-api.ts +++ b/lib/core/interface/public-api.ts @@ -16,5 +16,5 @@ */ export * from './authentication.interface'; -export * from './card-view-item.interface'; export * from './injection.tokens'; +export * from './search-configuration.interface'; diff --git a/lib/core/interface/search-configuration.interface.ts b/lib/core/interface/search-configuration.interface.ts new file mode 100644 index 0000000000..fe68d17edb --- /dev/null +++ b/lib/core/interface/search-configuration.interface.ts @@ -0,0 +1,24 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { QueryBody } from 'alfresco-js-api'; + +export interface SearchConfigurationInterface { + + generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody; + +} diff --git a/lib/core/login/components/login.component.spec.ts b/lib/core/login/components/login.component.spec.ts index 27345bf47e..59e197a643 100644 --- a/lib/core/login/components/login.component.spec.ts +++ b/lib/core/login/components/login.component.spec.ts @@ -26,8 +26,8 @@ import { AuthenticationService } from '../../services/authentication.service'; import { MaterialModule } from '../../material.module'; import { LoginErrorEvent } from '../models/login-error.event'; import { LoginSuccessEvent } from '../models/login-success.event'; -import { AuthenticationMock } from './../../mock/authentication.service.mock'; import { LoginComponent } from './login.component'; +import { Observable } from 'rxjs/Observable'; describe('LoginComponent', () => { let component: LoginComponent; @@ -64,7 +64,7 @@ describe('LoginComponent', () => { LoginComponent ], providers: [ - {provide: AuthenticationService, useClass: AuthenticationMock} + AuthenticationService ] }).compileComponents(); })); @@ -101,6 +101,7 @@ describe('LoginComponent', () => { } it('should redirect to route on successful login', () => { + spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'})); const redirect = '/home'; component.successRoute = redirect; spyOn(router, 'navigate'); @@ -109,9 +110,10 @@ describe('LoginComponent', () => { }); it('should redirect to previous route state on successful login', () => { + spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'})); const redirect = '/home'; component.successRoute = redirect; - authService.setRedirectUrl('redirect-url'); + authService.setRedirectUrl({ provider: 'ECM', url: 'redirect-url' } ); spyOn(router, 'navigate'); @@ -152,12 +154,14 @@ describe('LoginComponent', () => { }); it('should be changed back to the default after a failed login attempt', () => { + spyOn(authService, 'login').and.returnValue(Observable.throw('Fake server error')); loginWithCredentials('fake-wrong-username', 'fake-wrong-password'); expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.LOGIN'); }); it('should be changed to the "welcome key" after a successful login attempt', () => { + spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'})); loginWithCredentials('fake-username', 'fake-password'); expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.WELCOME'); @@ -436,6 +440,12 @@ describe('LoginComponent', () => { })); it('should return CORS error when server CORS error occurs', async(() => { + spyOn(authService, 'login').and.returnValue(Observable.throw({ + error: { + crossDomain: true, + message: 'ERROR: the network is offline, Origin is not allowed by Access-Control-Allow-Origin' + } + })); component.providers = 'ECM'; component.error.subscribe(() => { @@ -450,6 +460,8 @@ describe('LoginComponent', () => { })); it('should return CSRF error when server CSRF error occurs', async(() => { + spyOn(authService, 'login') + .and.returnValue(Observable.throw({ message: 'ERROR: Invalid CSRF-token', status: 403 })); component.providers = 'ECM'; component.error.subscribe(() => { @@ -464,6 +476,14 @@ describe('LoginComponent', () => { })); it('should return ECOM read-oly error when error occurs', async(() => { + spyOn(authService, 'login') + .and.returnValue( + Observable.throw( + { + message: 'ERROR: 00170728 Access Denied. The system is currently in read-only mode', + status: 403 + } + )); component.providers = 'ECM'; component.error.subscribe(() => { @@ -493,6 +513,7 @@ describe('LoginComponent', () => { })); it('should emit error event after the login has failed', async(() => { + spyOn(authService, 'login').and.returnValue(Observable.throw('Fake server error')); component.providers = 'ECM'; component.error.subscribe((error) => { diff --git a/lib/core/login/components/login.component.ts b/lib/core/login/components/login.component.ts index 0264d7093f..08743f4dd6 100644 --- a/lib/core/login/components/login.component.ts +++ b/lib/core/login/components/login.component.ts @@ -55,45 +55,61 @@ export class LoginComponent implements OnInit { isPasswordShow: boolean = false; + /* Should the `Remember me` checkbox be shown? */ @Input() showRememberMe: boolean = true; + /* Should the extra actions (`Need Help`, `Register`, etc) be shown? */ @Input() showLoginActions: boolean = true; + /* Sets the URL of the NEED HELP link in the footer */ @Input() needHelpLink: string = ''; + /* Sets the URL of the REGISTER link in the footer */ @Input() registerLink: string = ''; + /* Path to a custom logo image */ @Input() logoImageUrl: string = './assets/images/alfresco-logo.svg'; + /* Path to a custom background image */ @Input() backgroundImageUrl: string = './assets/images/background.svg'; + /* The copyright text below the login box */ @Input() copyrightText: string = '\u00A9 2016 Alfresco Software, Inc. All Rights Reserved.'; + /* Possible valid values are ECM, BPM or ALL. By default, this component + * will log in only to ECM. If you want to log in in both systems then use ALL. + */ @Input() providers: string; + /* Custom validation rules for the login form */ @Input() fieldsValidation: any; + /* Prevents the CSRF Token from being submitted. Only valid for Alfresco Process Services. */ @Input() disableCsrf: boolean; + /* Route to redirect to on successful login. */ @Input() successRoute: string = null; + /* Emitted when the login is successful */ @Output() success = new EventEmitter<LoginSuccessEvent>(); + /* Emitted when the login fails */ @Output() error = new EventEmitter<LoginErrorEvent>(); + /* Emitted when the login form is submitted */ @Output() executeSubmit = new EventEmitter<LoginSubmitEvent>(); @@ -197,7 +213,7 @@ export class LoginComponent implements OnInit { this.authService.login(values.username, values.password, this.rememberMe) .subscribe( (token: any) => { - const redirectUrl = this.authService.getRedirectUrl(); + const redirectUrl = this.authService.getRedirectUrl(this.providers); this.actualLoginStep = LoginSteps.Welcome; this.userPreferences.setStoragePrefix(values.username); diff --git a/lib/core/mock/traslation.service.mock.ts b/lib/core/mock/traslation.service.mock.ts index cb86c632b9..68e36cceb9 100644 --- a/lib/core/mock/traslation.service.mock.ts +++ b/lib/core/mock/traslation.service.mock.ts @@ -25,13 +25,17 @@ export interface LangChangeEvent { export class TranslationMock { - public onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>(); + onLangChange: EventEmitter<LangChangeEvent> = new EventEmitter<LangChangeEvent>(); addTranslationFolder() { } - public get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> { + get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> { return Observable.of(key); } + + instant(key: string | Array<string>, interpolateParams?: Object): string | any { + return key; + } } diff --git a/lib/core/models/public-api.ts b/lib/core/models/public-api.ts index 21f4d19b7c..e2e64f651e 100644 --- a/lib/core/models/public-api.ts +++ b/lib/core/models/public-api.ts @@ -15,14 +15,10 @@ * limitations under the License. */ -export * from './card-view-baseitem.model'; -export * from './card-view-textitem.model'; -export * from './card-view-mapitem.model'; -export * from './card-view-dateitem.model'; export * from './file.model'; export * from './permissions.enum'; -export * from './site.model'; export * from './product-version.model'; export * from './user-process.model'; export * from './comment-process.model'; export * from './ecm-company.model'; +export * from './redirection.model'; diff --git a/lib/core/models/redirection.model.ts b/lib/core/models/redirection.model.ts new file mode 100644 index 0000000000..c44359d843 --- /dev/null +++ b/lib/core/models/redirection.model.ts @@ -0,0 +1,33 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/** + * This object represent the user redirection guard.* + */ + +export class RedirectionModel { + provider: string; + url?: string; + + constructor(obj?: any) { + if (obj) { + this.provider = obj.provider; + this.url = obj.url || null; + } + } + +} diff --git a/lib/core/models/site.model.ts b/lib/core/models/site.model.ts deleted file mode 100644 index 540b452b18..0000000000 --- a/lib/core/models/site.model.ts +++ /dev/null @@ -1,91 +0,0 @@ -/*! - * @license - * Copyright 2016 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ - -import { Pagination } from 'alfresco-js-api'; - -export class SiteContentsModel { - id: string; - folderId: string; - - constructor(obj?: any) { - if (obj) { - this.id = obj.id || null; - this.folderId = obj.folderId || null; - } - } -} - -export class SiteMembersModel { - role: string; - firstName: string; - emailNotificationsEnabled: boolean = false; - company: any; - id: string; - enable: boolean = false; - email: string; - - constructor(obj?: any) { - if (obj) { - this.role = obj.role; - this.firstName = obj.firstName || null; - this.emailNotificationsEnabled = obj.emailNotificationsEnabled; - this.company = obj.company || null; - this.id = obj.id || null; - this.enable = obj.enable; - this.email = obj.email; - } - } -} - -export class SiteModel { - role: string; - visibility: string; - guid: string; - description: string; - id: string; - preset: string; - title: string; - contents: SiteContentsModel[] = []; - members: SiteMembersModel[] = []; - pagination: Pagination; - - constructor(obj?: any) { - if (obj && obj.entry) { - this.role = obj.entry.role || null; - this.visibility = obj.entry.visibility || null; - this.guid = obj.entry.guid || null; - this.description = obj.entry.description || null; - this.id = obj.entry.id || null; - this.preset = obj.entry.preset; - this.title = obj.entry.title; - this.pagination = obj.pagination || null; - - if (obj.relations && obj.relations.containers) { - obj.relations.containers.list.entries.forEach((content) => { - this.contents.push(new SiteContentsModel(content.entry)); - }); - } - - if (obj.relations && obj.relations.members) { - obj.relations.members.list.entries.forEach((member) => { - this.members.push(new SiteMembersModel(member.entry)); - }); - } - } - } - -} diff --git a/lib/core/package.json b/lib/core/package.json index da645a7af6..aad20cb270 100644 --- a/lib/core/package.json +++ b/lib/core/package.json @@ -1,7 +1,7 @@ { "name": "@alfresco/adf-core", "description": "Alfresco ADF core", - "version": "2.0.0", + "version": "2.1.0", "author": "Alfresco Software, Ltd.", "main": "bundles/adf-core.js", "repository": { @@ -12,21 +12,21 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "@angular/animations": "5.0.0", - "@angular/cdk": "5.0.0-rc0", - "@angular/common": "5.0.0", - "@angular/compiler": "5.0.0", - "@angular/core": "5.0.0", - "@angular/flex-layout": "2.0.0-beta.10", - "@angular/forms": "5.0.0", - "@angular/http": "5.0.0", - "@angular/material": "5.0.0-rc0", - "@angular/material-moment-adapter": "^5.0.0-rc0", - "@angular/platform-browser": "5.0.0", - "@angular/platform-browser-dynamic": "5.0.0", - "@angular/router": "5.0.0", + "@angular/animations": "5.1.1", + "@angular/cdk": "5.0.1", + "@angular/common": "5.1.1", + "@angular/compiler": "5.1.1", + "@angular/core": "5.1.1", + "@angular/flex-layout": "2.0.0-beta.12", + "@angular/forms": "5.1.1", + "@angular/http": "5.1.1", + "@angular/material": "5.0.1", + "@angular/material-moment-adapter": "5.0.1", + "@angular/platform-browser": "5.1.1", + "@angular/platform-browser-dynamic": "5.1.1", + "@angular/router": "5.1.1", "@ngx-translate/core": "8.0.0", - "alfresco-js-api": "2.0.0", + "alfresco-js-api": "2.1.0", "chart.js": "2.5.0", "core-js": "2.4.1", "hammerjs": "2.0.8", diff --git a/lib/core/pagination/infinite-pagination.component.spec.ts b/lib/core/pagination/infinite-pagination.component.spec.ts index 8243d80cbb..6b5a9c908b 100644 --- a/lib/core/pagination/infinite-pagination.component.spec.ts +++ b/lib/core/pagination/infinite-pagination.component.spec.ts @@ -21,6 +21,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Pagination } from 'alfresco-js-api'; import { MaterialModule } from '../material.module'; import { InfinitePaginationComponent } from './infinite-pagination.component'; +import { PaginatedComponent } from './paginated-component.interface'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; describe('InfinitePaginationComponent', () => { @@ -55,62 +57,111 @@ describe('InfinitePaginationComponent', () => { TestBed.resetTestingModule(); }); - it('should show the loading spinner if loading', () => { - pagination.hasMoreItems = true; - component.pagination = pagination; - component.isLoading = true; - fixture.detectChanges(); + describe('Standalone', () => { - let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); - expect(loadingSpinner).not.toBeNull(); - }); + it('should show the loading spinner if loading', () => { + pagination.hasMoreItems = true; + component.pagination = pagination; + component.isLoading = true; + fixture.detectChanges(); - it('should NOT show the loading spinner if NOT loading', () => { - pagination.hasMoreItems = true; - component.pagination = pagination; - component.isLoading = false; - fixture.detectChanges(); - - let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); - expect(loadingSpinner).toBeNull(); - }); - - it('should show the load more button if NOT loading and has more items', () => { - pagination.hasMoreItems = true; - component.pagination = pagination; - component.isLoading = false; - fixture.detectChanges(); - - let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); - expect(loadMoreButton).not.toBeNull(); - }); - - it('should NOT show anything if pagination has NO more items', () => { - pagination.hasMoreItems = false; - component.pagination = pagination; - fixture.detectChanges(); - - let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); - expect(loadMoreButton).toBeNull(); - let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); - expect(loadingSpinner).toBeNull(); - }); - - it('should trigger the loadMore event with the proper pagination object', (done) => { - pagination.hasMoreItems = true; - pagination.skipCount = 5; - component.pagination = pagination; - component.isLoading = false; - component.pageSize = 5; - fixture.detectChanges(); - - component.loadMore.subscribe((newPagination: Pagination) => { - expect(newPagination.skipCount).toBe(10); - done(); + let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); + expect(loadingSpinner).not.toBeNull(); }); - let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); - loadMoreButton.triggerEventHandler('click', {}); + it('should NOT show the loading spinner if NOT loading', () => { + pagination.hasMoreItems = true; + component.pagination = pagination; + component.isLoading = false; + fixture.detectChanges(); + + let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); + expect(loadingSpinner).toBeNull(); + }); + + it('should show the load more button if NOT loading and has more items', () => { + pagination.hasMoreItems = true; + component.pagination = pagination; + component.isLoading = false; + fixture.detectChanges(); + + let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); + expect(loadMoreButton).not.toBeNull(); + }); + + it('should NOT show anything if pagination has NO more items', () => { + pagination.hasMoreItems = false; + component.pagination = pagination; + fixture.detectChanges(); + + let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); + expect(loadMoreButton).toBeNull(); + let loadingSpinner = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-spinner"]')); + expect(loadingSpinner).toBeNull(); + }); + + it('should trigger the loadMore event with the proper pagination object', (done) => { + pagination.hasMoreItems = true; + pagination.skipCount = 5; + component.pagination = pagination; + component.isLoading = false; + component.pageSize = 5; + fixture.detectChanges(); + + component.loadMore.subscribe((newPagination: Pagination) => { + expect(newPagination.skipCount).toBe(10); + done(); + }); + + let loadMoreButton = fixture.debugElement.query(By.css('[data-automation-id="adf-infinite-pagination-button"]')); + loadMoreButton.triggerEventHandler('click', {}); + }); }); + describe('Target', () => { + + let testTarget: PaginatedComponent; + + beforeEach(() => { + + pagination = { maxItems: 444, skipCount: 0, totalItems: 888, hasMoreItems: true }; + testTarget = { + pagination: new BehaviorSubject<Pagination>(pagination), + supportedPageSizes: [], + updatePagination() {} + }; + + spyOn(testTarget, 'updatePagination'); + }); + + it('should subscribe to target\'s pagination observable to update pagination and pagesize correctly', () => { + component.target = testTarget; + fixture.detectChanges(); + + expect(component.pagination).toBe(pagination); + expect(component.pageSize).toBe(pagination.maxItems); + }); + + it('should call the target\'s updatePagination on invoking the onLoadMore', () => { + component.target = testTarget; + fixture.detectChanges(); + + component.onLoadMore(); + + expect(testTarget.updatePagination).toHaveBeenCalledWith({ maxItems: 444, skipCount: 444, totalItems: 888, hasMoreItems: true }); + }); + + it('should unsubscribe from the target\'s pagination on onDestroy', () => { + component.target = testTarget; + fixture.detectChanges(); + fixture.destroy(); + + const emitNewPaginationEvent = () => { + let newPagination = { maxItems: 1, skipCount: 0, totalItems: 2, hasMoreItems: true }; + testTarget.pagination.next(newPagination); + }; + + expect(emitNewPaginationEvent).not.toThrow(); + }); + }); }); diff --git a/lib/core/pagination/infinite-pagination.component.ts b/lib/core/pagination/infinite-pagination.component.ts index 891813ab64..203b9d8a57 100644 --- a/lib/core/pagination/infinite-pagination.component.ts +++ b/lib/core/pagination/infinite-pagination.component.ts @@ -15,17 +15,23 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, + OnDestroy, ViewEncapsulation } from '@angular/core'; - +import { PaginatedComponent } from './paginated-component.interface'; +import { PaginationQueryParams } from './pagination-query-params.interface'; import { Pagination } from 'alfresco-js-api'; +import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'adf-infinite-pagination', @@ -35,7 +41,7 @@ import { Pagination } from 'alfresco-js-api'; changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None }) -export class InfinitePaginationComponent implements OnInit { +export class InfinitePaginationComponent implements OnInit, OnDestroy { static DEFAULT_PAGE_SIZE: number = 25; @@ -47,6 +53,9 @@ export class InfinitePaginationComponent implements OnInit { @Input() pagination: Pagination; + @Input() + target: PaginatedComponent; + @Input() pageSize: number = InfinitePaginationComponent.DEFAULT_PAGE_SIZE; @@ -56,7 +65,19 @@ export class InfinitePaginationComponent implements OnInit { @Output() loadMore: EventEmitter<Pagination> = new EventEmitter<Pagination>(); + private paginationSubscription: Subscription; + + constructor(private cdr: ChangeDetectorRef) {} + ngOnInit() { + if (this.target) { + this.paginationSubscription = this.target.pagination.subscribe(page => { + this.pagination = page; + this.pageSize = page.maxItems; + this.cdr.detectChanges(); + }); + } + if (!this.pagination) { this.pagination = InfinitePaginationComponent.DEFAULT_PAGINATION; } @@ -65,5 +86,15 @@ export class InfinitePaginationComponent implements OnInit { onLoadMore() { this.pagination.skipCount += this.pageSize; this.loadMore.next(this.pagination); + + if (this.target) { + this.target.updatePagination(<PaginationQueryParams> this.pagination); + } + } + + ngOnDestroy() { + if (this.paginationSubscription) { + this.paginationSubscription.unsubscribe(); + } } } diff --git a/lib/core/pagination/paginated-component.interface.ts b/lib/core/pagination/paginated-component.interface.ts index d745ddda80..34bfd53733 100644 --- a/lib/core/pagination/paginated-component.interface.ts +++ b/lib/core/pagination/paginated-component.interface.ts @@ -16,14 +16,12 @@ */ import { Pagination } from 'alfresco-js-api'; -import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { PaginationQueryParams } from './pagination-query-params.interface'; export interface PaginatedComponent { - - pagination: Subject<Pagination>; + pagination: BehaviorSubject<Pagination>; supportedPageSizes: number[]; updatePagination(params: PaginationQueryParams); - } diff --git a/lib/core/pagination/pagination.component.spec.ts b/lib/core/pagination/pagination.component.spec.ts index 21b3e32c10..24c4819f4d 100644 --- a/lib/core/pagination/pagination.component.spec.ts +++ b/lib/core/pagination/pagination.component.spec.ts @@ -27,7 +27,7 @@ import { TranslateLoaderService } from '../services/translate-loader.service'; import { TranslationService } from '../services/translation.service'; import { PaginationComponent } from './pagination.component'; import { PaginatedComponent } from './public-api'; -import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; class FakePaginationInput implements Pagination { count: number = 25; @@ -91,6 +91,11 @@ describe('PaginationComponent', () => { }); })); + it('should have an "empty" class if no items present', () => { + fixture.detectChanges(); + expect(fixture.nativeElement.classList.contains('adf-pagination__empty')).toBeTruthy(); + }); + describe('Single page', () => { beforeEach(() => { component.pagination = new FakePaginationInput(1, 1, 10); @@ -274,7 +279,7 @@ describe('PaginationComponent', () => { const pagination: Pagination = {}; const customComponent = <PaginatedComponent> { - pagination: new Subject<Pagination>() + pagination: new BehaviorSubject<Pagination>({}) }; component.target = customComponent; @@ -289,7 +294,7 @@ describe('PaginationComponent', () => { const pagination2: Pagination = {}; const customComponent = <PaginatedComponent> { - pagination: new Subject<Pagination>() + pagination: new BehaviorSubject<Pagination>({}) }; component.target = customComponent; @@ -304,7 +309,7 @@ describe('PaginationComponent', () => { it('should send pagination event to paginated component', () => { const customComponent = <PaginatedComponent> { - pagination: new Subject<Pagination>(), + pagination: new BehaviorSubject<Pagination>({}), updatePagination() {}, supportedPageSizes: [] }; diff --git a/lib/core/pagination/pagination.component.ts b/lib/core/pagination/pagination.component.ts index 779c99bf0d..c161002f73 100644 --- a/lib/core/pagination/pagination.component.ts +++ b/lib/core/pagination/pagination.component.ts @@ -24,7 +24,8 @@ import { Output, ViewEncapsulation, ChangeDetectorRef, - OnDestroy + OnDestroy, + HostBinding } from '@angular/core'; import { Pagination } from 'alfresco-js-api'; @@ -134,6 +135,11 @@ export class PaginationComponent implements OnInit, OnDestroy { return this.pagination && this.pagination.count > 0; } + @HostBinding('class.adf-pagination__empty') + get isEmpty(): boolean { + return !this.hasItems; + } + get range(): number[] { const { skipCount, maxItems, totalItems } = this.pagination; const { isLastPage } = this; diff --git a/lib/core/pipes/file-size.pipe.spec.ts b/lib/core/pipes/file-size.pipe.spec.ts index 0e177ad691..e2b0a2f0a8 100644 --- a/lib/core/pipes/file-size.pipe.spec.ts +++ b/lib/core/pipes/file-size.pipe.spec.ts @@ -22,7 +22,23 @@ describe('FileSizePipe', () => { let pipe: FileSizePipe; beforeEach(() => { - pipe = new FileSizePipe(); + const translation: any = { + instant(key) { + const enUs = { + 'CORE.FILE_SIZE.BYTES': 'Bytes', + 'CORE.FILE_SIZE.KB': 'KB', + 'CORE.FILE_SIZE.MB': 'MB', + 'CORE.FILE_SIZE.GB': 'GB', + 'CORE.FILE_SIZE.TB': 'TB', + 'CORE.FILE_SIZE.PB': 'PB', + 'CORE.FILE_SIZE.EB': 'EB', + 'CORE.FILE_SIZE.ZB': 'ZB', + 'CORE.FILE_SIZE.YB': 'YB' + }; + return enUs[key]; + } + }; + pipe = new FileSizePipe(translation); }); it('returns empty string with invalid input', () => { diff --git a/lib/core/pipes/file-size.pipe.ts b/lib/core/pipes/file-size.pipe.ts index ac28ba22fb..12cb6f1b9e 100644 --- a/lib/core/pipes/file-size.pipe.ts +++ b/lib/core/pipes/file-size.pipe.ts @@ -16,27 +16,34 @@ */ import { Pipe, PipeTransform } from '@angular/core'; +import { TranslationService } from '../services/translation.service'; @Pipe({ - name: 'adfFileSize' + name: 'adfFileSize', + pure: false }) export class FileSizePipe implements PipeTransform { + constructor(private translation: TranslationService) { + } + transform(bytes: number, decimals: number = 2): string { if (bytes == null || bytes === undefined) { return ''; } if (bytes === 0) { - return '0 Bytes'; + return '0 ' + this.translation.instant('CORE.FILE_SIZE.BYTES'); } const k = 1024, dm = decimals || 2, - sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + sizes = ['BYTES', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + const i18nSize = this.translation.instant(`CORE.FILE_SIZE.${sizes[i]}`); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + i18nSize; } } diff --git a/lib/core/services/alfresco-api.service.ts b/lib/core/services/alfresco-api.service.ts index 3f739691d2..a16baa73d8 100644 --- a/lib/core/services/alfresco-api.service.ts +++ b/lib/core/services/alfresco-api.service.ts @@ -19,7 +19,7 @@ import { Injectable } from '@angular/core'; import { AlfrescoApi, ContentApi, FavoritesApi, NodesApi, PeopleApi, RenditionsApi, SharedlinksApi, SitesApi, - VersionsApi + VersionsApi, ClassesApi } from 'alfresco-js-api'; import * as alfrescoApi from 'alfresco-js-api'; import { AppConfigService } from '../app-config/app-config.service'; @@ -70,6 +70,10 @@ export class AlfrescoApiService { return this.getInstance().core.versionsApi; } + get classesApi(): ClassesApi { + return this.getInstance().core.classesApi; + } + constructor(private appConfig: AppConfigService, private storage: StorageService) { diff --git a/lib/core/services/auth-guard-bpm.service.spec.ts b/lib/core/services/auth-guard-bpm.service.spec.ts index 0ec1f504e0..86e8c20b70 100644 --- a/lib/core/services/auth-guard-bpm.service.spec.ts +++ b/lib/core/services/auth-guard-bpm.service.spec.ts @@ -15,14 +15,14 @@ * limitations under the License. */ -import { async, inject, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { async, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { CookieServiceMock } from './../mock/cookie.service.mock'; import { AlfrescoApiService } from './alfresco-api.service'; import { SettingsService } from './settings.service'; import { AppConfigModule } from '../app-config/app-config.module'; +import { AppConfigService } from '../app-config/app-config.service'; import { AuthGuardBpm } from './auth-guard-bpm.service'; import { AuthenticationService } from './authentication.service'; import { CookieService } from './cookie.service'; @@ -30,9 +30,15 @@ import { LogService } from './log.service'; import { StorageService } from './storage.service'; import { TranslateLoaderService } from './translate-loader.service'; import { UserPreferencesService } from './user-preferences.service'; +import { RouterStateSnapshot, Router } from '@angular/router'; describe('AuthGuardService BPM', () => { + let authGuard: AuthGuardBpm; + let authService: AuthenticationService; + let routerService: Router; + let appConfigService: AppConfigService; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ @@ -58,43 +64,58 @@ describe('AuthGuardService BPM', () => { }).compileComponents(); })); - it('if the alfresco js api is logged in should canActivate be true', - async(inject([AuthGuardBpm, Router, SettingsService, StorageService, AuthenticationService], (auth, router, settingsService, storage, authService) => { - spyOn(router, 'navigate'); + beforeEach(() => { + authService = TestBed.get(AuthenticationService); + authGuard = TestBed.get(AuthGuardBpm); + routerService = TestBed.get(Router); + appConfigService = TestBed.get(AppConfigService); + }); - authService.isBpmLoggedIn = () => { - return true; - }; + it('if the alfresco js api is logged in should canActivate be true', async(() => { + spyOn(authService, 'isBpmLoggedIn').and.returnValue(true); + const router: RouterStateSnapshot = <RouterStateSnapshot> {url : ''}; - expect(auth.canActivate(null, { url: '' })).toBeTruthy(); - expect(router.navigate).not.toHaveBeenCalled(); - })) - ); + expect(authGuard.canActivate(null, router)).toBeTruthy(); + })); - it('if the alfresco js api is NOT logged in should canActivate be false', - async(inject([AuthGuardBpm, Router, SettingsService, StorageService, AuthenticationService], (auth, router, settingsService, storage, authService) => { + it('if the alfresco js api is NOT logged in should canActivate be false', async(() => { + spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); + spyOn(routerService, 'navigate').and.stub(); + const router: RouterStateSnapshot = <RouterStateSnapshot> { url: '' }; - spyOn(router, 'navigate'); + expect(authGuard.canActivate(null, router)).toBeFalsy(); + })); - authService.isBpmLoggedIn = () => { - return false; - }; + it('if the alfresco js api is NOT logged in should trigger a redirect event', async(() => { + spyOn(routerService, 'navigate'); + spyOn(authService, 'isBpmLoggedIn').and.returnValue(false); + const router: RouterStateSnapshot = <RouterStateSnapshot> {url : ''}; - expect(auth.canActivate(null, { url: '' })).toBeFalsy(); - expect(router.navigate).toHaveBeenCalled(); - })) - ); + expect(authGuard.canActivate(null, router)).toBeFalsy(); + expect(routerService.navigate).toHaveBeenCalledWith(['/login']); + })); - it('should set redirect url', - async(inject([AuthGuardBpm, Router, AuthenticationService], (auth, router, authService) => { - const state = { url: 'some-url' }; + it('should set redirect url', async(() => { + spyOn(authService, 'setRedirectUrl').and.callThrough(); + spyOn(routerService, 'navigate').and.stub(); + const router: RouterStateSnapshot = <RouterStateSnapshot> { url: 'some-url' }; - spyOn(router, 'navigate'); - spyOn(authService, 'setRedirectUrl'); + authGuard.canActivate(null, router); - auth.canActivate(null , state); + expect(authService.setRedirectUrl).toHaveBeenCalledWith({provider: 'BPM', url: 'some-url' } ); + expect(authService.getRedirectUrl('BPM')).toBe('some-url'); + })); + + it('should get redirect url from config if there is one configured', async(() => { + appConfigService.config.loginRoute = 'fakeLoginRoute'; + spyOn(authService, 'setRedirectUrl').and.callThrough(); + spyOn(routerService, 'navigate').and.stub(); + const router: RouterStateSnapshot = <RouterStateSnapshot> { url: 'some-url' }; + + authGuard.canActivate(null, router); + + expect(authService.setRedirectUrl).toHaveBeenCalledWith({provider: 'BPM', url: 'some-url' } ); + expect(routerService.navigate).toHaveBeenCalledWith(['/fakeLoginRoute']); + })); - expect(authService.setRedirectUrl).toHaveBeenCalledWith(state.url); - })) - ); }); diff --git a/lib/core/services/auth-guard-bpm.service.ts b/lib/core/services/auth-guard-bpm.service.ts index 9f302bf93d..7aa826e4e0 100644 --- a/lib/core/services/auth-guard-bpm.service.ts +++ b/lib/core/services/auth-guard-bpm.service.ts @@ -18,15 +18,16 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, - Router, - RouterStateSnapshot + RouterStateSnapshot, Router } from '@angular/router'; - +import { AppConfigService } from '../app-config/app-config.service'; import { AuthenticationService } from './authentication.service'; @Injectable() export class AuthGuardBpm implements CanActivate, CanActivateChild { - constructor(private authService: AuthenticationService, private router: Router) {} + constructor(private authService: AuthenticationService, + private router: Router, + private appConfig: AppConfigService) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.checkLogin(state.url); @@ -41,9 +42,16 @@ export class AuthGuardBpm implements CanActivate, CanActivateChild { return true; } - this.authService.setRedirectUrl(redirectUrl); + this.authService.setRedirectUrl({ provider: 'BPM', url: redirectUrl }); + const pathToLogin = this.getRouteDestinationForLogin(); + this.router.navigate(['/' + pathToLogin]); - this.router.navigate(['/login']); return false; } + + private getRouteDestinationForLogin(): string { + return this.appConfig && + this.appConfig.get<string>('loginRoute') ? + this.appConfig.get<string>('loginRoute') : 'login'; + } } diff --git a/lib/core/services/auth-guard-ecm.service.spec.ts b/lib/core/services/auth-guard-ecm.service.spec.ts index 42a78586d9..7f02eeeaec 100644 --- a/lib/core/services/auth-guard-ecm.service.spec.ts +++ b/lib/core/services/auth-guard-ecm.service.spec.ts @@ -125,7 +125,7 @@ class TestConfig { } } -describe('CanActivateLoggedIn', () => { +describe('AuthGuardService ECM', () => { describe('user is not logged in', () => { beforeEach(async(() => { this.test = new TestConfig({ @@ -211,7 +211,7 @@ describe('CanActivateLoggedIn', () => { })); it('should set redirect url', () => { - expect(this.auth.setRedirectUrl).toHaveBeenCalledWith('some-url'); + expect(this.auth.setRedirectUrl).toHaveBeenCalledWith({ provider: 'ECM', url: 'some-url' }); }); }); diff --git a/lib/core/services/auth-guard-ecm.service.ts b/lib/core/services/auth-guard-ecm.service.ts index 3858d305b8..28ef5119ef 100644 --- a/lib/core/services/auth-guard-ecm.service.ts +++ b/lib/core/services/auth-guard-ecm.service.ts @@ -16,16 +16,18 @@ */ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, Router } from '@angular/router'; import { AlfrescoApiService } from './alfresco-api.service'; import { AuthenticationService } from './authentication.service'; +import { AppConfigService } from '../app-config/app-config.service'; @Injectable() export class AuthGuardEcm implements CanActivate { constructor( private authService: AuthenticationService, private apiService: AlfrescoApiService, - private router: Router) { + private router: Router, + private appConfig: AppConfigService) { } private get authApi() { @@ -51,11 +53,18 @@ export class AuthGuardEcm implements CanActivate { return this.isLoggedIn().then(isLoggedIn => { if (!isLoggedIn) { - this.authService.setRedirectUrl(state.url); - this.router.navigate([ '/login' ]); + this.authService.setRedirectUrl({ provider: 'ECM', url: state.url }); + const pathToLogin = this.getRouteDestinationForLogin(); + this.router.navigate(['/' + pathToLogin]); } return isLoggedIn; }); } + + private getRouteDestinationForLogin(): string { + return this.appConfig && + this.appConfig.get<string>('loginRoute') ? + this.appConfig.get<string>('loginRoute') : 'login'; + } } diff --git a/lib/core/services/auth-guard.service.spec.ts b/lib/core/services/auth-guard.service.spec.ts index a5f21bc583..35771a43a2 100644 --- a/lib/core/services/auth-guard.service.spec.ts +++ b/lib/core/services/auth-guard.service.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { async, inject, TestBed } from '@angular/core/testing'; +import { async, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -24,6 +24,7 @@ import { CookieServiceMock } from './../mock/cookie.service.mock'; import { AlfrescoApiService } from './alfresco-api.service'; import { SettingsService } from './settings.service'; import { AppConfigModule } from '../app-config/app-config.module'; +import { AppConfigService } from '../app-config/app-config.service'; import { AuthGuard } from './auth-guard.service'; import { AuthenticationService } from './authentication.service'; import { CookieService } from './cookie.service'; @@ -34,6 +35,10 @@ import { UserPreferencesService } from './user-preferences.service'; describe('AuthGuardService', () => { let state; + let authService: AuthenticationService; + let router: Router; + let service: AuthGuard; + let appConfigService: AppConfigService; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -62,45 +67,50 @@ describe('AuthGuardService', () => { beforeEach(() => { state = { url: '' }; + authService = TestBed.get(AuthenticationService); + router = TestBed.get(Router); + service = TestBed.get(AuthGuard); + appConfigService = TestBed.get(AppConfigService); }); - it('if the alfresco js api is logged in should canActivate be true', - async(inject([AuthGuard, Router, SettingsService, StorageService, AuthenticationService], (auth, router, settingsService, storage, authService) => { - spyOn(router, 'navigate'); + it('if the alfresco js api is logged in should canActivate be true', async(() => { + spyOn(router, 'navigate'); + spyOn(authService, 'isLoggedIn').and.returnValue(true); - authService.isLoggedIn = () => { - return true; - }; + expect(service.canActivate(null, state)).toBeTruthy(); + expect(router.navigate).not.toHaveBeenCalled(); + })); - expect(auth.canActivate(null, state)).toBeTruthy(); - expect(router.navigate).not.toHaveBeenCalled(); - })) - ); + it('if the alfresco js api is NOT logged in should canActivate be false', async(() => { + spyOn(router, 'navigate'); + spyOn(authService, 'isLoggedIn').and.returnValue(false); - it('if the alfresco js api is NOT logged in should canActivate be false', - async(inject([AuthGuard, Router, SettingsService, StorageService, AuthenticationService], (auth, router, settingsService, storage, authService) => { + expect(service.canActivate(null, state)).toBeFalsy(); + expect(router.navigate).toHaveBeenCalled(); + })); - spyOn(router, 'navigate'); + it('should set redirect url', async(() => { + state.url = 'some-url'; - authService.isLoggedIn = () => { - return false; - }; + spyOn(router, 'navigate'); + spyOn(authService, 'setRedirectUrl'); - expect(auth.canActivate(null, state)).toBeFalsy(); - expect(router.navigate).toHaveBeenCalled(); - })) - ); + service.canActivate(null, state); - it('should set redirect url', - async(inject([AuthGuard, Router, AuthenticationService], (auth, router, authService) => { - state.url = 'some-url'; + expect(authService.setRedirectUrl).toHaveBeenCalledWith({ provider: 'ALL', url: 'some-url' }); + expect(router.navigate).toHaveBeenCalledWith(['/login']); + })); - spyOn(router, 'navigate'); - spyOn(authService, 'setRedirectUrl'); + it('should get redirect url from config if there is one configured', async(() => { + state.url = 'some-url'; + appConfigService.config.loginRoute = 'fakeLoginRoute'; - auth.canActivate(null , state); + spyOn(router, 'navigate'); + spyOn(authService, 'setRedirectUrl'); - expect(authService.setRedirectUrl).toHaveBeenCalledWith(state.url); - })) - ); + service.canActivate(null, state); + + expect(authService.setRedirectUrl).toHaveBeenCalledWith({ provider: 'ALL', url: 'some-url' }); + expect(router.navigate).toHaveBeenCalledWith(['/fakeLoginRoute']); + })); }); diff --git a/lib/core/services/auth-guard.service.ts b/lib/core/services/auth-guard.service.ts index d10cd781d4..ff6224e5f5 100644 --- a/lib/core/services/auth-guard.service.ts +++ b/lib/core/services/auth-guard.service.ts @@ -17,16 +17,17 @@ import { Injectable } from '@angular/core'; import { - ActivatedRouteSnapshot, CanActivate, CanActivateChild, - Router, - RouterStateSnapshot + ActivatedRouteSnapshot, CanActivate, + CanActivateChild, RouterStateSnapshot, Router } from '@angular/router'; - +import { AppConfigService } from '../app-config/app-config.service'; import { AuthenticationService } from './authentication.service'; @Injectable() export class AuthGuard implements CanActivate, CanActivateChild { - constructor(private authService: AuthenticationService, private router: Router) {} + constructor(private authService: AuthenticationService, + private router: Router, + private appConfig: AppConfigService) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { const redirectUrl = state.url; @@ -43,9 +44,16 @@ export class AuthGuard implements CanActivate, CanActivateChild { return true; } - this.authService.setRedirectUrl(redirectUrl); + this.authService.setRedirectUrl({ provider: 'ALL', url: redirectUrl } ); + const pathToLogin = this.getRouteDestinationForLogin(); + this.router.navigate(['/' + pathToLogin]); - this.router.navigate(['/login']); return false; } + + private getRouteDestinationForLogin(): string { + return this.appConfig && + this.appConfig.get<string>('loginRoute') ? + this.appConfig.get<string>('loginRoute') : 'login'; + } } diff --git a/lib/core/services/authentication.service.spec.ts b/lib/core/services/authentication.service.spec.ts index 1f0cf9e6a1..4f25dd591c 100644 --- a/lib/core/services/authentication.service.spec.ts +++ b/lib/core/services/authentication.service.spec.ts @@ -258,6 +258,24 @@ describe('AuthenticationService', () => { expect(authService.isLoggedIn()).toBe(false); expect(authService.isEcmLoggedIn()).toBe(false); }); + + it('[ECM] should set/get redirectUrl when provider is ECM', () => { + authService.setRedirectUrl({provider: 'ECM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBe('some-url'); + }); + + it('[ECM] should set/get redirectUrl when provider is BPM', () => { + authService.setRedirectUrl({provider: 'BPM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBeNull(); + }); + + it('[ECM] should return null as redirectUrl when redirectUrl field is not set', () => { + authService.setRedirectUrl( null ); + + expect(authService.getRedirectUrl(preferences.authType)).toBeNull(); + }); }); describe('when the setting is BPM', () => { @@ -367,6 +385,24 @@ describe('AuthenticationService', () => { 'status': 403 }); }); + + it('[BPM] should set/get redirectUrl when provider is BPM', () => { + authService.setRedirectUrl({provider: 'BPM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBe('some-url'); + }); + + it('[BPM] should set/get redirectUrl when provider is ECM', () => { + authService.setRedirectUrl({provider: 'ECM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBeNull(); + }); + + it('[BPM] should return null as redirectUrl when redirectUrl field is not set', () => { + authService.setRedirectUrl( null ); + + expect(authService.getRedirectUrl(preferences.authType)).toBeNull(); + }); }); describe('when the setting is both ECM and BPM ', () => { @@ -396,7 +432,7 @@ describe('AuthenticationService', () => { }); }); - xit('[ALL] should return login fail if only ECM call fail', (done) => { + it('[ALL] should return login fail if only ECM call fail', (done) => { authService.login('fake-username', 'fake-password').subscribe( (res) => { }, @@ -417,7 +453,7 @@ describe('AuthenticationService', () => { }); }); - xit('[ALL] should return login fail if only BPM call fail', (done) => { + it('[ALL] should return login fail if only BPM call fail', (done) => { authService.login('fake-username', 'fake-password').subscribe( (res) => { }, @@ -461,11 +497,30 @@ describe('AuthenticationService', () => { 'status': 403 }); }); + + it('[ALL] should set/get redirectUrl when provider is ALL', () => { + authService.setRedirectUrl({provider: 'ALL', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBe('some-url'); + }); + + it('[ALL] should set/get redirectUrl when provider is BPM', () => { + authService.setRedirectUrl({provider: 'BPM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBe('some-url'); + }); + + it('[ALL] should set/get redirectUrl when provider is ECM', () => { + authService.setRedirectUrl({provider: 'ECM', url: 'some-url' } ); + + expect(authService.getRedirectUrl(preferences.authType)).toBe('some-url'); + }); + + it('[ALL] should return null as redirectUrl when redirectUrl field is not set', () => { + authService.setRedirectUrl( null ); + + expect(authService.getRedirectUrl(preferences.authType)).toBeNull(); + }); }); - it('should set/get redirectUrl', () => { - authService.setRedirectUrl('some-url'); - - expect(authService.getRedirectUrl()).toBe('some-url'); - }); }); diff --git a/lib/core/services/authentication.service.ts b/lib/core/services/authentication.service.ts index 4ca675ee29..5c4821b6cc 100644 --- a/lib/core/services/authentication.service.ts +++ b/lib/core/services/authentication.service.ts @@ -26,13 +26,14 @@ import { UserPreferencesService } from './user-preferences.service'; import 'rxjs/add/observable/fromPromise'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; +import { RedirectionModel } from '../models/redirection.model'; const REMEMBER_ME_COOKIE_KEY = 'ALFRESCO_REMEMBER_ME'; const REMEMBER_ME_UNTIL = 1000 * 60 * 60 * 24 * 30 ; @Injectable() export class AuthenticationService { - private redirectUrl: string = ''; + private redirectUrl: RedirectionModel = null; onLogin: Subject<any> = new Subject<any>(); onLogout: Subject<any> = new Subject<any>(); @@ -233,12 +234,20 @@ export class AuthenticationService { return this.alfrescoApi.getInstance().bpmAuth.username; } - setRedirectUrl(url: string) { + setRedirectUrl(url: RedirectionModel) { this.redirectUrl = url; } - getRedirectUrl(): string { - return this.redirectUrl; + getRedirectUrl(provider: string): string { + return this.hasValidRedirection(provider) ? this.redirectUrl.url : null; + } + + private hasValidRedirection(provider: string): boolean { + return this.redirectUrl && (this.redirectUrl.provider === provider || this.hasSelectedProviderAll(provider)); + } + + private hasSelectedProviderAll(provider: string): boolean { + return this.redirectUrl && (this.redirectUrl.provider === 'ALL' || provider === 'ALL'); } /** diff --git a/lib/core/services/content.service.ts b/lib/core/services/content.service.ts index 3661863b77..50c7a606de 100644 --- a/lib/core/services/content.service.ts +++ b/lib/core/services/content.service.ts @@ -133,35 +133,35 @@ export class ContentService { /** * Get thumbnail URL for the given document node. * - * @param {string|MinimalNodeEntity} nodeId Node to get URL for. + * @param {string|MinimalNodeEntity} nodeId or node to get URL for. * @param {boolean} [attachment] Retrieve content as an attachment for download * @param {string} [ticket] Custom ticket to use for authentication * @returns {string} The URL address pointing to the content. */ - getDocumentThumbnailUrl(nodeId: any, attachment?: boolean, ticket?: string): string { + getDocumentThumbnailUrl(node: any, attachment?: boolean, ticket?: string): string { - if (nodeId && nodeId.entry) { - nodeId = nodeId.entry.id; + if (node && node.entry) { + node = node.entry.id; } - return this.contentApi.getDocumentThumbnailUrl(nodeId, attachment, ticket); + return this.contentApi.getDocumentThumbnailUrl(node, attachment, ticket); } /** * Get content URL for the given node. * - * @param nodeId {string|MinimalNodeEntity} Node to get URL for. + * @param node {string|MinimalNodeEntity} nodeId or node to get URL for. * @param {boolean} [attachment] Retrieve content as an attachment for download * @param {string} [ticket] Custom ticket to use for authentication * @returns {string} The URL address pointing to the content. */ - getContentUrl(nodeId: any, attachment?: boolean, ticket?: string): string { + getContentUrl(node: any, attachment?: boolean, ticket?: string): string { - if (nodeId && nodeId.entry) { - nodeId = nodeId.entry.id; + if (node && node.entry) { + node = node.entry.id; } - return this.contentApi.getContentUrl(nodeId, attachment, ticket); + return this.contentApi.getContentUrl(node, attachment, ticket); } /** diff --git a/lib/core/services/dynamic-component-mapper.service.ts b/lib/core/services/dynamic-component-mapper.service.ts index c654dce9f1..b8a6d1eaac 100644 --- a/lib/core/services/dynamic-component-mapper.service.ts +++ b/lib/core/services/dynamic-component-mapper.service.ts @@ -16,14 +16,13 @@ */ import { Type } from '@angular/core'; +import { getType } from './get-type'; export interface DynamicComponentModel { type: string; } export type DynamicComponentResolveFunction = (model: DynamicComponentModel) => Type<{}>; export class DynamicComponentResolver { static fromType(type: Type<{}>): DynamicComponentResolveFunction { - return (model: DynamicComponentModel) => { - return type; - }; + return getType(type); } } diff --git a/lib/core/services/favorites-api.service.ts b/lib/core/services/favorites-api.service.ts index 25a501d459..930d4c3575 100644 --- a/lib/core/services/favorites-api.service.ts +++ b/lib/core/services/favorites-api.service.ts @@ -23,16 +23,6 @@ import { UserPreferencesService } from './user-preferences.service'; @Injectable() export class FavoritesApiService { - static remapFavoritesData(data: any = {}): NodePaging { - const list = (data.list || {}); - const pagination = (list.pagination || {}); - const entries: any[] = FavoritesApiService - .remapFavoriteEntries(list.entries || []); - - return <NodePaging> { - list: { entries, pagination } - }; - } static remapEntry({ entry }: any): any { entry.properties = { @@ -43,7 +33,18 @@ export class FavoritesApiService { return { entry }; } - static remapFavoriteEntries(entries: any[]) { + remapFavoritesData(data: any = {}): NodePaging { + const list = (data.list || {}); + const pagination = (list.pagination || {}); + const entries: any[] = this + .remapFavoriteEntries(list.entries || []); + + return <NodePaging> { + list: { entries, pagination } + }; + } + + remapFavoriteEntries(entries: any[]) { return entries .map(({ entry: { target }}: any) => ({ entry: target.file || target.folder @@ -72,7 +73,7 @@ export class FavoritesApiService { const queryOptions = Object.assign(defaultOptions, options); const promise = favoritesApi .getFavorites(personId, queryOptions) - .then(FavoritesApiService.remapFavoritesData); + .then(this.remapFavoritesData); return Observable .fromPromise(promise) diff --git a/lib/core/services/get-type.ts b/lib/core/services/get-type.ts new file mode 100644 index 0000000000..2329cbc3d9 --- /dev/null +++ b/lib/core/services/get-type.ts @@ -0,0 +1,20 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export function getType(type: any): any { + return () => type; +} diff --git a/lib/core/services/public-api.ts b/lib/core/services/public-api.ts index 053cfdff7c..f9eea442a1 100644 --- a/lib/core/services/public-api.ts +++ b/lib/core/services/public-api.ts @@ -34,8 +34,6 @@ export * from './translate-loader.service'; export * from './thumbnail.service'; export * from './upload.service'; export * from './dynamic-component-mapper.service'; -export * from './card-item-types.service'; -export * from './card-view-update.service'; export * from './user-preferences.service'; export * from './highlight-transform.service'; export * from './deleted-nodes-api.service'; @@ -48,3 +46,4 @@ export * from './shared-links-api.service'; export * from './sites.service'; export * from './discovery-api.service'; export * from './comment-process.service'; +export * from './search-configuration.service'; diff --git a/lib/core/services/renditions.service.ts b/lib/core/services/renditions.service.ts index 21718b1cb4..3ccc69af02 100644 --- a/lib/core/services/renditions.service.ts +++ b/lib/core/services/renditions.service.ts @@ -71,7 +71,7 @@ export class RenditionsService { } getRenditionUrl(nodeId: string, encoding: string): string { - return this.apiService.contentApi.getRenditionUrl(nodeId, 'pdf'); + return this.apiService.contentApi.getRenditionUrl(nodeId, encoding); } getRendition(nodeId: string, encoding: string): Observable<RenditionEntry> { diff --git a/lib/core/services/search-configuration.service.ts b/lib/core/services/search-configuration.service.ts new file mode 100644 index 0000000000..92d490d382 --- /dev/null +++ b/lib/core/services/search-configuration.service.ts @@ -0,0 +1,45 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Injectable } from '@angular/core'; +import { QueryBody } from 'alfresco-js-api'; +import { SearchConfigurationInterface } from '../interface/search-configuration.interface'; + +@Injectable() +export class SearchConfigurationService implements SearchConfigurationInterface { + + constructor() { + } + + public generateQueryBody(searchTerm: string, maxResults: string, skipCount: string): QueryBody { + let defaultQueryBody: QueryBody = { + query: { + query: searchTerm ? `${searchTerm}* OR name:${searchTerm}*` : searchTerm + }, + include: ['path', 'allowableOperations'], + paging: { + maxItems: maxResults, + skipCount: skipCount + }, + filterQueries: [ + { query: "TYPE:'cm:folder' OR TYPE:'cm:content'" }, + { query: 'NOT cm:creator:System' }] + }; + + return defaultQueryBody; + } +} diff --git a/lib/core/services/search.service.ts b/lib/core/services/search.service.ts index fa7806bed7..bcf345ca59 100644 --- a/lib/core/services/search.service.ts +++ b/lib/core/services/search.service.ts @@ -21,15 +21,14 @@ import { Observable } from 'rxjs/Observable'; import { AlfrescoApiService } from './alfresco-api.service'; import { AuthenticationService } from './authentication.service'; import 'rxjs/add/observable/throw'; +import { SearchConfigurationService } from './search-configuration.service'; -/** - * Internal service used by Document List component. - */ @Injectable() export class SearchService { constructor(public authService: AuthenticationService, - private apiService: AlfrescoApiService) { + private apiService: AlfrescoApiService, + private searchConfigurationService: SearchConfigurationService) { } getNodeQueryResults(term: string, options?: SearchOptions): Observable<NodePaging> { @@ -38,8 +37,8 @@ export class SearchService { .catch(err => this.handleError(err)); } - search(query: QueryBody): Observable<NodePaging> { - const searchQuery = Object.assign(query); + search(searchTerm: string, maxResults: string, skipCount: string): Observable<NodePaging> { + const searchQuery = Object.assign(this.searchConfigurationService.generateQueryBody(searchTerm, maxResults, skipCount)); const promise = this.apiService.getInstance().search.searchApi.search(searchQuery); return Observable @@ -47,6 +46,14 @@ export class SearchService { .catch(err => this.handleError(err)); } + searchByQueryBody(queryBody: QueryBody): Observable<NodePaging> { + const promise = this.apiService.getInstance().search.searchApi.search(queryBody); + + return Observable + .fromPromise(promise) + .catch(err => this.handleError(err)); + } + private handleError(error: any): Observable<any> { return Observable.throw(error || 'Server error'); } diff --git a/lib/core/services/service.module.ts b/lib/core/services/service.module.ts index 2a60514026..4a905442b7 100644 --- a/lib/core/services/service.module.ts +++ b/lib/core/services/service.module.ts @@ -23,8 +23,6 @@ import { AuthGuardBpm } from './auth-guard-bpm.service'; import { AuthGuardEcm } from './auth-guard-ecm.service'; import { AuthGuard } from './auth-guard.service'; import { AuthenticationService } from './authentication.service'; -import { CardItemTypeService } from './card-item-types.service'; -import { CardViewUpdateService } from './card-view-update.service'; import { CommentProcessService } from './comment-process.service'; import { ContentService } from './content.service'; import { CookieService } from './cookie.service'; @@ -49,6 +47,7 @@ import { TranslateLoaderService } from './translate-loader.service'; import { TranslationService } from './translation.service'; import { UploadService } from './upload.service'; import { UserPreferencesService } from './user-preferences.service'; +import { SearchConfigurationService } from './search-configuration.service'; @NgModule({ imports: [], @@ -72,8 +71,6 @@ import { UserPreferencesService } from './user-preferences.service'; TranslateLoaderService, ThumbnailService, UploadService, - CardItemTypeService, - CardViewUpdateService, UserPreferencesService, HighlightTransformService, DeletedNodesApiService, @@ -85,7 +82,8 @@ import { UserPreferencesService } from './user-preferences.service'; SharedLinksApiService, SitesService, DiscoveryApiService, - CommentProcessService + CommentProcessService, + SearchConfigurationService ], exports: [ ] diff --git a/lib/core/services/sites-api.spec.ts b/lib/core/services/sites.service.spec.ts similarity index 97% rename from lib/core/services/sites-api.spec.ts rename to lib/core/services/sites.service.spec.ts index a9a55de4d3..39abcb24a5 100644 --- a/lib/core/services/sites-api.spec.ts +++ b/lib/core/services/sites.service.spec.ts @@ -76,7 +76,7 @@ describe('Sites service', () => { it('Should get a list of users sites', (done) => { service.getSites().subscribe((data) => { - expect(data[0].title).toBe('FAKE'); + expect(data.list.entries[0].entry.title).toBe('FAKE'); done(); }); @@ -111,7 +111,7 @@ describe('Sites service', () => { it('Should get single sites via siteId', (done) => { service.getSite('fake-site-id').subscribe((data) => { - expect(data.title).toBe('FAKE-SINGLE-TITLE'); + expect(data.entry.title).toBe('FAKE-SINGLE-TITLE'); done(); }); diff --git a/lib/core/services/sites.service.ts b/lib/core/services/sites.service.ts index ce71f8b7e3..e669a1fd53 100644 --- a/lib/core/services/sites.service.ts +++ b/lib/core/services/sites.service.ts @@ -18,10 +18,10 @@ import { Injectable } from '@angular/core'; import { Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; -import { SiteModel } from '../models/site.model'; import { AlfrescoApiService } from './alfresco-api.service'; import 'rxjs/add/observable/fromPromise'; import 'rxjs/add/operator/catch'; +import { SitePaging, SiteEntry } from 'alfresco-js-api'; @Injectable() export class SitesService { @@ -29,35 +29,33 @@ export class SitesService { constructor( private apiService: AlfrescoApiService) { } - getSites(opts: any = {}): any { + getSites(opts: any = {}): Observable<SitePaging> { const defaultOptions = { skipCount: 0, include: ['properties'] }; const queryOptions = Object.assign({}, defaultOptions, opts); return Observable.fromPromise(this.apiService.getInstance().core.sitesApi.getSites(queryOptions)) - .map((res) => this.convertToModel(res)) .catch(this.handleError); } - getSite(siteId: string, opts?: any): any { + getSite(siteId: string, opts?: any): Observable<SiteEntry> { return Observable.fromPromise(this.apiService.getInstance().core.sitesApi.getSite(siteId, opts)) - .map((res: any) => new SiteModel(res)) .catch(this.handleError); } - deleteSite(siteId: string, permanentFlag: boolean = true): any { + deleteSite(siteId: string, permanentFlag: boolean = true): Observable<any> { let options: any = {}; options.permanent = permanentFlag; return Observable.fromPromise(this.apiService.getInstance().core.sitesApi.deleteSite(siteId, options) .catch(this.handleError)); } - getSiteContent(siteId: string): Observable<any> { + getSiteContent(siteId: string): Observable<SiteEntry> { return this.getSite(siteId, { relations: ['containers'] }); } - getSiteMembers(siteId: string): Observable<any> { + getSiteMembers(siteId: string): Observable<SiteEntry> { return this.getSite(siteId, { relations: ['members'] }); } @@ -65,18 +63,4 @@ export class SitesService { console.error(error); return Observable.throw(error || 'Server error'); } - - private convertToModel(response: any) { - let convertedList: SiteModel[] = []; - if (response && - response.list && - response.list.entries && - response.list.entries.length > 0) { - response.list.entries.forEach((element: any) => { - element.pagination = response.list.pagination; - convertedList.push(new SiteModel(element)); - }); - } - return convertedList; - } } diff --git a/lib/core/services/thumbnail.service.ts b/lib/core/services/thumbnail.service.ts index 973f4c5a96..a5a1dae1ab 100644 --- a/lib/core/services/thumbnail.service.ts +++ b/lib/core/services/thumbnail.service.ts @@ -125,8 +125,8 @@ export class ThumbnailService { * @param document Node to get URL for. * @returns {string} URL address. */ - public getDocumentThumbnailUrl(document: any): string { - let thumbnail = this.contentService.getDocumentThumbnailUrl(document); + public getDocumentThumbnailUrl(node: any): string { + let thumbnail = this.contentService.getDocumentThumbnailUrl(node); return thumbnail || this.DEFAULT_ICON; } diff --git a/lib/core/services/translate-loader.service.ts b/lib/core/services/translate-loader.service.ts index 727b04b557..d5e7ac819b 100644 --- a/lib/core/services/translate-loader.service.ts +++ b/lib/core/services/translate-loader.service.ts @@ -60,8 +60,10 @@ export class TranslateLoaderService implements TranslateLoader { if (!this.isComponentInQueue(lang, component.name)) { this.queue[lang].push(component.name); - let currentObserv = Observable.create(observer => { - this.http.get(`${component.path}/${this.prefix}/${lang}${this.suffix}`) + const loader = Observable.create(observer => { + const translationUrl = `${component.path}/${this.prefix}/${lang}${this.suffix}?v=${Date.now()}`; + + this.http.get(translationUrl) .map((res: Response) => { component.json[lang] = res; }).subscribe((result) => { @@ -73,7 +75,7 @@ export class TranslateLoaderService implements TranslateLoader { }); }); - observableBatch.push(currentObserv); + observableBatch.push(loader); } }); diff --git a/lib/core/services/translation.service.ts b/lib/core/services/translation.service.ts index 99cd087744..5cba275066 100644 --- a/lib/core/services/translation.service.ts +++ b/lib/core/services/translation.service.ts @@ -91,4 +91,8 @@ export class TranslationService { get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> { return this.translate.get(key, interpolateParams); } + + instant(key: string | Array<string>, interpolateParams?: Object): string | any { + return this.translate.instant(key, interpolateParams); + } } diff --git a/lib/core/services/user-preferences.service.spec.ts b/lib/core/services/user-preferences.service.spec.ts index a0eab114a8..3a613bee54 100644 --- a/lib/core/services/user-preferences.service.spec.ts +++ b/lib/core/services/user-preferences.service.spec.ts @@ -28,6 +28,7 @@ import { UserPreferencesService } from './user-preferences.service'; describe('UserPreferencesService', () => { const defaultPaginationSize: number = 10; + const supportedPaginationSize = [5, 10, 15, 20]; let preferences: UserPreferencesService; let storage: StorageService; let appConfig: AppConfigService; @@ -55,7 +56,8 @@ describe('UserPreferencesService', () => { appConfig = TestBed.get(AppConfigService); appConfig.config = { pagination: { - size: 10 + 'size': 10, + 'supportedPageSizes': [ 5, 10, 15, 20 ] } }; preferences = TestBed.get(UserPreferencesService); @@ -64,7 +66,12 @@ describe('UserPreferencesService', () => { }); it('should get default pagination from app config', () => { - expect(preferences.paginationSize).toBe(defaultPaginationSize); + expect(preferences.defaults.paginationSize).toBe(defaultPaginationSize); + }); + + it('should return supported page sizes defined in the app config', () => { + const supportedPages = preferences.getDifferentPageSizes(); + expect(supportedPages).toEqual(supportedPaginationSize); }); it('should use [GUEST] as default storage prefix', () => { diff --git a/lib/core/services/user-preferences.service.ts b/lib/core/services/user-preferences.service.ts index a8210551ab..acf7092653 100644 --- a/lib/core/services/user-preferences.service.ts +++ b/lib/core/services/user-preferences.service.ts @@ -26,8 +26,9 @@ import { StorageService } from './storage.service'; @Injectable() export class UserPreferencesService { - private defaults = { + defaults = { paginationSize: 25, + supportedPageSizes: [5, 10, 15, 20], locale: 'en' }; @@ -43,7 +44,8 @@ export class UserPreferencesService { const currentLocale = this.locale || this.getDefaultLocale(); this.localeSubject = new BehaviorSubject(currentLocale); this.locale$ = this.localeSubject.asObservable(); - this.defaults.paginationSize = appConfig.get('pagination.size', 25); + this.defaults.paginationSize = this.appConfig.get('pagination.size', this.defaults.paginationSize); + this.defaults.supportedPageSizes = this.appConfig.get('pagination.supportedPageSizes', this.defaults.supportedPageSizes); } get(property: string, defaultValue?: string): string { @@ -76,6 +78,10 @@ export class UserPreferencesService { return `${this.getStoragePrefix()}__${property}`; } + getDifferentPageSizes(): number[] { + return this.defaults.supportedPageSizes; + } + set authType(value: string) { this.storage.setItem('AUTH_TYPE', value); this.apiService.reset(); diff --git a/lib/core/sidebar/index.ts b/lib/core/sidebar/index.ts new file mode 100644 index 0000000000..4c6ac1d58f --- /dev/null +++ b/lib/core/sidebar/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './public-api'; diff --git a/lib/core/sidebar/public-api.ts b/lib/core/sidebar/public-api.ts new file mode 100644 index 0000000000..40f4feee03 --- /dev/null +++ b/lib/core/sidebar/public-api.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './sidebar-action-menu.component'; diff --git a/lib/core/sidebar/sidebar-action-menu.component.html b/lib/core/sidebar/sidebar-action-menu.component.html new file mode 100644 index 0000000000..88e0273aab --- /dev/null +++ b/lib/core/sidebar/sidebar-action-menu.component.html @@ -0,0 +1,16 @@ +<div class="adf-sidebar-action-menu"> + <button *ngIf="isExpanded()" mat-raised-button class="adf-sidebar-action-menu-button" data-automation-id="create-button" [matMenuTriggerFor]="adfSidebarMenu"> + <span *ngIf="title" class="adf-sidebar-action-menu-text">{{ title }}</span> + <ng-content select="[sidebar-menu-title-icon]"></ng-content> + </button> + + <div *ngIf="!isExpanded()" class="adf-sidebar-action-menu-icon" [matMenuTriggerFor]="adfSidebarMenu"> + <ng-content select="[sidebar-menu-expand-icon]"></ng-content> + </div> + + <mat-menu #adfSidebarMenu="matMenu" class="adf-sidebar-action-menu-panel" [overlapTrigger]="false" yPosition="below"> + <div class="adf-sidebar-action-menu-options"> + <ng-content select="[sidebar-menu-options]"></ng-content> + </div> + </mat-menu> +</div> diff --git a/lib/core/sidebar/sidebar-action-menu.component.scss b/lib/core/sidebar/sidebar-action-menu.component.scss new file mode 100644 index 0000000000..3b7a47b59b --- /dev/null +++ b/lib/core/sidebar/sidebar-action-menu.component.scss @@ -0,0 +1,63 @@ +@mixin adf-sidebar-action-menu-theme($theme) { + + $primary: map-get($theme, primary); + $foreground: map-get($theme, foreground); + + $adf-sidebar-action-menu-button-height: 37.5px; + $adf-sidebar-action-menu-button-width: 202px; + $adf-sidebar-action-menu-button-border-radius: 4px; + $adf-sidebar-action-menu-opacity: 0.54; + $adf-sidebar-action-menu-item-opacity: 0.87; + $adf-sidebar-action-menu-item-line-spacing: -0.4px; + + .adf { + &-sidebar-action-menu { + & .mat-raised-button { + width: $adf-sidebar-action-menu-button-width; + height: $adf-sidebar-action-menu-button-height; + font-weight: bold; + background-color: mat-color($primary); + color: mat-color($primary, default-contrast) !important; + border-radius: $adf-sidebar-action-menu-button-border-radius; + & mat-icon { + width: 24px; + height: 25px; + color: mat-color($primary, default-contrast) !important; + } + } + &-text { + width: 100%; + height: 20px; + text-align: left; + } + } + &-sidebar-action-menu-icon { + margin: 18px 0px 0px 20px; + color: mat-color($foreground, text, $adf-sidebar-action-menu-opacity); + cursor: pointer; + &:hover { + color: mat-color($primary); + } + } + &-sidebar-action-menu-options { + text-align: left; + letter-spacing: $adf-sidebar-action-menu-item-line-spacing; + .mat-menu-item { + color: mat-color($foreground, text, $adf-sidebar-action-menu-item-opacity); + text-align: left; + line-height: 1.5; + letter-spacing: $adf-sidebar-action-menu-item-line-spacing; + } + .mat-menu-item:hover { + color: mat-color($primary); + opacity: inherit; + } + } + &-sidebar-action-menu-panel { + margin-top: 7.5px; + border-radius: 2px; + box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.26), 0 0 8px 0 rgba(0, 0, 0, 0.12); + min-width: $adf-sidebar-action-menu-button-width !important; + } + } +} \ No newline at end of file diff --git a/lib/core/sidebar/sidebar-action-menu.component.spec.ts b/lib/core/sidebar/sidebar-action-menu.component.spec.ts new file mode 100644 index 0000000000..da75d50d74 --- /dev/null +++ b/lib/core/sidebar/sidebar-action-menu.component.spec.ts @@ -0,0 +1,149 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Component } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { MaterialModule } from '../material.module'; +import { SidebarActionMenuComponent } from './sidebar-action-menu.component'; + +describe('SidebarActionMenuComponent', () => { + let element: HTMLElement; + let component: SidebarActionMenuComponent; + let fixture: ComponentFixture<SidebarActionMenuComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + SidebarActionMenuComponent + ], + imports: [ + MaterialModule + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SidebarActionMenuComponent); + element = fixture.nativeElement; + component = fixture.componentInstance; + }); + + it('should create instance of SidebarActionMenuComponent', () => { + expect(fixture.componentInstance instanceof SidebarActionMenuComponent).toBe(true, 'should create SidebarActionMenuComponent'); + }); + + it('should display title', () => { + component.title = 'Fake-Title'; + component.expanded = true; + fixture.detectChanges(); + const title = element.querySelector('.adf-sidebar-action-menu-text'); + fixture.detectChanges(); + expect(title.textContent).toBe('Fake-Title'); + }); +}); + +@Component({ + template: ` + <adf-sidebar-action-menu [expanded]="expanded" [title]="title"> + <mat-icon sidebar-menu-title-icon>arrow_drop_down</mat-icon> + <div sidebar-menu-expand-icon> + <mat-icon>queue</mat-icon> + </div> + <div sidebar-menu-options> + <button mat-menu-item> + <mat-icon>assignment</mat-icon> + <span>Option1</span> + </button> + <button mat-menu-item> + <mat-icon>assignment</mat-icon> + <span>Option2</span> + </button> + </div> +</adf-sidebar-action-menu> + ` +}) +class CustomSidebarActionMenuComponent { + title: string = 'Fake title'; + expanded: boolean = true; +} + +describe('Custom SidebarActionMenuComponent', () => { + let fixture: ComponentFixture<CustomSidebarActionMenuComponent>; + let component: CustomSidebarActionMenuComponent; + let element: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + SidebarActionMenuComponent, + CustomSidebarActionMenuComponent + ], + imports: [ + MaterialModule + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomSidebarActionMenuComponent); + fixture.detectChanges(); + component = fixture.componentInstance; + element = fixture.nativeElement; + }); + + it('should create instance of CustomSidebarActionMenuComponent', () => { + expect(component instanceof CustomSidebarActionMenuComponent).toBe(true, 'should create CustomSidebarActionMenuComponent'); + }); + + it('should defined adf-sidebar-action-menu', () => { + fixture.detectChanges(); + element = fixture.nativeElement.querySelector('adf-sidebar-action-menu'); + expect(element).toBeDefined(); + }); + + it('should display the title', () => { + component.title = 'FakeTitle'; + fixture.detectChanges(); + const title = element.querySelector('.adf-sidebar-action-menu-text'); + fixture.detectChanges(); + expect(title.textContent).toBe('FakeTitle'); + }); + + it('should render the sidebar-menu-options', () => { + fixture.detectChanges(); + const actionButton = fixture.nativeElement.querySelector('.adf-sidebar-action-menu-button'); + const options = fixture.nativeElement.querySelectorAll('.adf-sidebar-action-menu-options'); + actionButton.click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(actionButton).not.toBeNull(); + expect(actionButton).toBeDefined(); + expect(options).toBeDefined(); + expect(actionButton.innerText.trim()).toBe('Fake title arrow_drop_down'); + }); + }); + + it('should show icon on icon menu', () => { + component.title = 'FakeTitle'; + component.expanded = false; + fixture.detectChanges(); + const actionIcon = fixture.nativeElement.querySelector('.adf-sidebar-action-menu-icon'); + expect(actionIcon).not.toBeNull(); + expect(actionIcon).toBeDefined(); + expect(actionIcon.innerText.trim()).toBe('queue'); + }); +}); diff --git a/lib/core/sidebar/sidebar-action-menu.component.ts b/lib/core/sidebar/sidebar-action-menu.component.ts new file mode 100644 index 0000000000..b082d56904 --- /dev/null +++ b/lib/core/sidebar/sidebar-action-menu.component.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { ChangeDetectionStrategy, Component, Directive, Input, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'adf-sidebar-action-menu', + templateUrl: './sidebar-action-menu.component.html', + styleUrls: ['./sidebar-action-menu.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { 'class': 'adf-sidebar-action-menu' } +}) + +export class SidebarActionMenuComponent { + + @Input() + title: string; + + @Input() + expanded: boolean; + + isExpanded(): boolean { + return this.expanded; + } +} + +@Directive({ selector: '[sidebar-menu-options]' }) export class SidebarMenuDirective {} +@Directive({ selector: '[sidebar-menu-title-icon]' }) export class SidebarMenuTitleIconDirective { } +@Directive({ selector: '[sidebar-menu-expand-icon]' }) export class SidebarMenuExpandIconDirective { } diff --git a/lib/core/sidebar/sidebar-action.module.ts b/lib/core/sidebar/sidebar-action.module.ts new file mode 100644 index 0000000000..7750d0ac66 --- /dev/null +++ b/lib/core/sidebar/sidebar-action.module.ts @@ -0,0 +1,38 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CommonModule } from '@angular/common'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { NgModule } from '@angular/core'; +import { MaterialModule } from '../material.module'; + +import { SidebarActionMenuComponent } from './sidebar-action-menu.component'; + +@NgModule({ + imports: [ + CommonModule, + MaterialModule, + FlexLayoutModule + ], + declarations: [ + SidebarActionMenuComponent + ], + exports: [ + SidebarActionMenuComponent + ] +}) +export class SideBarActionModule {} diff --git a/lib/core/styles/_index.scss b/lib/core/styles/_index.scss index 9868291429..e26c0ba8ed 100644 --- a/lib/core/styles/_index.scss +++ b/lib/core/styles/_index.scss @@ -2,10 +2,7 @@ @import './default-class'; @import './theming'; - -@import '../card-view/card-view-dateitem.component'; -@import '../card-view/card-view-textitem.component'; -@import '../card-view/card-view.component'; +@import '../card-view/card-view.module'; @import '../collapsable/accordion-group.component'; @import '../datatable/components/datatable/datatable.component'; @import '../form/components/widgets/container/container.widget'; @@ -20,13 +17,14 @@ @import '../toolbar/toolbar.component'; @import '../userinfo/components/user-info.component'; @import '../viewer/components/viewer.component'; +@import '../form/components/form.component'; +@import '../sidebar/sidebar-action-menu.component'; +@import '../viewer/components/pdfViewer.component'; @mixin adf-core-theme($theme) { @include adf-colors-theme($theme); @include adf-default-class-theme($theme); - @include adf-card-view-dateitem-theme($theme); - @include adf-card-view-textitem-theme($theme); - @include adf-card-view-theme($theme); + @include adf-card-view-module-theme($theme); @include adf-accordion-theme($theme); @include adf-datatable-theme($theme); @include adf-form-container-widget-theme($theme); @@ -41,6 +39,9 @@ @include adf-toolbar-theme($theme); @include adf-userinfo-theme($theme); @include adf-viewer-theme($theme); + @include adf-pdf-viewer-theme($theme); + @include adf-form-component-theme($theme); + @include adf-sidebar-action-menu-theme($theme); } diff --git a/lib/core/styles/_mixins.scss b/lib/core/styles/_mixins.scss index 6a1ac6994f..d2e2b69d24 100644 --- a/lib/core/styles/_mixins.scss +++ b/lib/core/styles/_mixins.scss @@ -1,30 +1,60 @@ -@import 'variables'; +@import "variables"; @mixin no-select { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } -@mixin typo-icon() { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - word-wrap: normal; - font-feature-settings: 'liga'; - -webkit-font-feature-settings: 'liga'; - -webkit-font-smoothing: antialiased; +@mixin typo-icon { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + word-wrap: normal; + font-feature-settings: 'liga'; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; } -@mixin material-animation-default($duration:0.2s) { - transition-duration: $duration; - transition-timing-function: $animation-curve-default; +@mixin material-animation-default($duration: 0.2s) { + transition-duration: $duration; + transition-timing-function: $animation-curve-default; +} + +@mixin line-clamp($line-height: 1.25, $lines: 3) { + display: block; + display: -webkit-box; + -webkit-box-orient: vertical; + position: relative; + line-height: $line-height; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 !important; + -webkit-line-clamp: $lines; + height: calc(0.99em * #{$line-height} * #{$lines}); + + &:after { + content: '...'; + text-align: right; + bottom: 0; + right: 0; + width: 25%; + display: block; + position: absolute; + height: calc(0.99em * #{$line-height}); + } +} + +@supports (-webkit-line-clamp: 1) { + .line-clamp:after { + display: none !important; + } } diff --git a/lib/core/userinfo/components/user-info.component.spec.ts b/lib/core/userinfo/components/user-info.component.spec.ts index 194220426f..59beb48d5a 100644 --- a/lib/core/userinfo/components/user-info.component.spec.ts +++ b/lib/core/userinfo/components/user-info.component.spec.ts @@ -29,35 +29,35 @@ import { UserInfoComponent } from './user-info.component'; class FakeSanitazer extends DomSanitizer { - constructor() { - super(); - } - - sanitize(html) { - return html; - } - - bypassSecurityTrustHtml(value: string): any { - return value; - } - - bypassSecurityTrustStyle(value: string): any { - return null; - } - - bypassSecurityTrustScript(value: string): any { - return null; - } - - bypassSecurityTrustUrl(value: string): any { - return null; - } - - bypassSecurityTrustResourceUrl(value: string): any { - return null; - } + constructor() { + super(); } + sanitize(html) { + return html; + } + + bypassSecurityTrustHtml(value: string): any { + return value; + } + + bypassSecurityTrustStyle(value: string): any { + return null; + } + + bypassSecurityTrustScript(value: string): any { + return null; + } + + bypassSecurityTrustUrl(value: string): any { + return null; + } + + bypassSecurityTrustResourceUrl(value: string): any { + return null; + } +} + declare let jasmine: any; describe('User info component', () => { @@ -119,7 +119,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmEditedUser}) + responseText: JSON.stringify({ entry: fakeEcmEditedUser }) }); fixture.whenStable().then(() => { @@ -141,7 +141,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmEditedUser}) + responseText: JSON.stringify({ entry: fakeEcmEditedUser }) }); fixture.whenStable().then(() => { @@ -157,7 +157,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmEditedUser}) + responseText: JSON.stringify({ entry: fakeEcmEditedUser }) }); fixture.whenStable().then(() => { @@ -171,7 +171,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmEditedUser}) + responseText: JSON.stringify({ entry: fakeEcmEditedUser }) }); fixture.whenStable().then(() => { @@ -186,7 +186,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmEditedUser}) + responseText: JSON.stringify({ entry: fakeEcmEditedUser }) }); fixture.whenStable().then(() => { @@ -203,7 +203,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmUser}) + responseText: JSON.stringify({ entry: fakeEcmUser }) }); })); @@ -262,7 +262,7 @@ describe('User info component', () => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmUserNoImage}) + responseText: JSON.stringify({ entry: fakeEcmUserNoImage }) }); fixture.whenStable().then(() => fixture.detectChanges()); })); @@ -417,20 +417,23 @@ describe('User info component', () => { describe('when user is logged on bpm and ecm', () => { + beforeEach(async(() => { + + })); + beforeEach(async(() => { spyOn(stubAuthService, 'isEcmLoggedIn').and.returnValue(true); spyOn(stubAuthService, 'isBpmLoggedIn').and.returnValue(true); spyOn(stubAuthService, 'isLoggedIn').and.returnValue(true); spyOn(stubContent, 'getContentUrl').and.returnValue('src/assets/images/ecmImg.gif'); jasmine.Ajax.install(); - })); - beforeEach(async(() => { fixture.detectChanges(); + jasmine.Ajax.requests.first().respondWith({ status: 200, contentType: 'application/json', - responseText: JSON.stringify({entry: fakeEcmUser}) + responseText: JSON.stringify({ entry: fakeEcmUser }) }); jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, @@ -441,8 +444,10 @@ describe('User info component', () => { beforeEach(() => { fixture.detectChanges(); + let imageButton: HTMLButtonElement = <HTMLButtonElement> element.querySelector('#logged-user-img'); imageButton.click(); + fixture.detectChanges(); }); @@ -457,24 +462,28 @@ describe('User info component', () => { let bpmUsername = fixture.debugElement.query(By.css('#bpm-username')); let bpmImage = fixture.debugElement.query(By.css('#bpm-user-detail-image')); - expect(element.querySelector('#userinfo_container')).not.toBeNull(); - expect(bpmUsername).not.toBeNull(); - expect(bpmImage).not.toBeNull(); - expect(bpmImage.properties.src).toContain('app/rest/admin/profile-picture'); - expect(bpmUsername.nativeElement.textContent).toContain('fake-bpm-first-name fake-bpm-last-name'); - expect(fixture.debugElement.query(By.css('#bpm-tenant')).nativeElement.textContent).toContain('fake-tenant-name'); + fixture.whenStable().then(() => { + expect(element.querySelector('#userinfo_container')).not.toBeNull(); + expect(bpmUsername).not.toBeNull(); + expect(bpmImage).not.toBeNull(); + expect(bpmImage.properties.src).toContain('app/rest/admin/profile-picture'); + expect(bpmUsername.nativeElement.textContent).toContain('fake-bpm-first-name fake-bpm-last-name'); + expect(fixture.debugElement.query(By.css('#bpm-tenant')).nativeElement.textContent).toContain('fake-tenant-name'); + }); }); it('should get the ecm user informations from the service', () => { let ecmUsername = fixture.debugElement.query(By.css('#ecm-username')); let ecmImage = fixture.debugElement.query(By.css('#ecm-user-detail-image')); - expect(element.querySelector('#userinfo_container')).toBeDefined(); - expect(ecmUsername).not.toBeNull(); - expect(ecmImage).not.toBeNull(); - expect(ecmImage.properties.src).toContain('assets/images/ecmImg.gif'); - expect(fixture.debugElement.query(By.css('#ecm-full-name')).nativeElement.textContent).toContain('fake-ecm-first-name fake-ecm-last-name'); - expect(fixture.debugElement.query(By.css('#ecm-job-title')).nativeElement.textContent).toContain('job-ecm-test'); + fixture.whenStable().then(() => { + expect(element.querySelector('#userinfo_container')).toBeDefined(); + expect(ecmUsername).not.toBeNull(); + expect(ecmImage).not.toBeNull(); + expect(ecmImage.properties.src).toContain('assets/images/ecmImg.gif'); + expect(fixture.debugElement.query(By.css('#ecm-full-name')).nativeElement.textContent).toContain('fake-ecm-first-name fake-ecm-last-name'); + expect(fixture.debugElement.query(By.css('#ecm-job-title')).nativeElement.textContent).toContain('job-ecm-test'); + }); }); it('should show the ecm image if exists', () => { diff --git a/lib/core/viewer/components/pdfViewer.component.html b/lib/core/viewer/components/pdfViewer.component.html index a7bd395f93..dedb69d260 100644 --- a/lib/core/viewer/components/pdfViewer.component.html +++ b/lib/core/viewer/components/pdfViewer.component.html @@ -34,15 +34,14 @@ <mat-icon>keyboard_arrow_down</mat-icon> </button> - <div> - {{ 'ADF_VIEWER.PAGE_LABEL.SHOWING' | translate }} + <div class="adf-pdf-viewer__toolbar-page-selector"> + <span>{{ 'ADF_VIEWER.PAGE_LABEL.SHOWING' | translate }}</span> <input #page - class="adf-pdf-viewer__toolbar-page-selector" type="text" pattern="-?[0-9]*(\.[0-9]+)?" value="{{ displayPage }}" (keyup.enter)="inputPage(page.value)"> - {{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }} + <span>{{ 'ADF_VIEWER.PAGE_LABEL.OF' | translate }} {{ totalPages }}</span> </div> <adf-toolbar-divider></adf-toolbar-divider> diff --git a/lib/core/viewer/components/pdfViewer.component.scss b/lib/core/viewer/components/pdfViewer.component.scss index 0efe4c0b69..7770305700 100644 --- a/lib/core/viewer/components/pdfViewer.component.scss +++ b/lib/core/viewer/components/pdfViewer.component.scss @@ -1,38 +1,53 @@ -.adf-pdf-viewer { - .loader-container { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - -webkit-box-flex-direction: row; - -moz-box-flex-direction: row; - -webkit-flex-direction: row; - flex-direction: row; - height:100%; - } +@mixin adf-pdf-viewer-theme($theme) { + .adf-pdf-viewer { + .loader-container { + display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ + display: -webkit-flex; /* NEW - Chrome */ + display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + -webkit-box-flex-direction: row; + -moz-box-flex-direction: row; + -webkit-flex-direction: row; + flex-direction: row; + height:100%; + } - .loader-item { - margin: auto; - max-height:100px; - max-width:300px; - } + .loader-item { + margin: auto; + max-height:100px; + max-width:300px; + } - &__toolbar { - position: absolute; - bottom: 0; + &__toolbar { + position: absolute; + bottom: 5px; - left: 50%; - transform: translateX(-50%); + left: 50%; + transform: translateX(-50%); - &-page-selector { - font-size: 16px; - padding: 4px 0; - text-align: right; - width: 33px; - margin-right: 4px; - height: 20px; - outline-width: 1px; - outline-color: gray; + .adf-toolbar .mat-toolbar { + max-height: 48px; + background-color: mat-color($primary, default-contrast, 1); + border-width: 0; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.24), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + + &-page-selector { + padding-left: 10px; + padding-right: 10px; + font-size: 14px; + + & > input { + font-size: 14px; + padding: 4px 0; + text-align: right; + width: 33px; + margin-right: 4px; + outline-width: 1px; + outline-color: gray; + } + } } } } diff --git a/lib/core/viewer/components/pdfViewerHost.component.scss b/lib/core/viewer/components/pdfViewerHost.component.scss index 1f2503d46c..a9d85f6504 100644 --- a/lib/core/viewer/components/pdfViewerHost.component.scss +++ b/lib/core/viewer/components/pdfViewerHost.component.scss @@ -165,6 +165,11 @@ right: 0; bottom: 0; } + + * { + padding: 0; + margin: 0; + } } &.removePageBorders { @@ -202,10 +207,7 @@ } } - * { - padding: 0; - margin: 0; - } + .hidden, [hidden] { display: none !important; diff --git a/lib/core/viewer/components/txtViewer.component.scss b/lib/core/viewer/components/txtViewer.component.scss index 4b58d2e074..a3c88afc85 100644 --- a/lib/core/viewer/components/txtViewer.component.scss +++ b/lib/core/viewer/components/txtViewer.component.scss @@ -3,5 +3,4 @@ width: 100vw; overflow: hidden; overflow-y: scroll; - margin-bottom: 42px; } diff --git a/lib/core/viewer/components/viewer.component.html b/lib/core/viewer/components/viewer.component.html index 019b091725..212923b8ef 100644 --- a/lib/core/viewer/components/viewer.component.html +++ b/lib/core/viewer/components/viewer.component.html @@ -18,7 +18,7 @@ <mat-icon>arrow_back</mat-icon> </button> <img class="adf-viewer__mimeicon" [src]="mimeType | adfMimeTypeIcon"> - <span id="adf-viewer-display-name">{{ displayName }}</span> + <span class="adf-viewer__display-name" id="adf-viewer-display-name">{{ displayName }}</span> </adf-toolbar-title> <ng-container *ngIf="mnuOpenWith"> @@ -84,7 +84,7 @@ matTooltip="{{ 'ADF_VIEWER.ACTIONS.INFO' | translate }}" data-automation-id="toolbar-sidebar" [color]="showSidebar ? 'accent' : 'default'" - (click)="showSidebar = !showSidebar"> + (click)="toggleSidebar($event)"> <mat-icon>info_outline</mat-icon> </button> </ng-container> @@ -103,7 +103,7 @@ <div *ngIf="!isLoading" fxLayout="row" fxFlex="1 1 auto"> - <ng-container *ngIf="showSidebar"> + <ng-container *ngIf="allowSidebar && showSidebar"> <div class="adf-viewer__sidebar" fxFlexOrder="{{sidebarPosition === 'left'? 1: 2 }}"> <ng-container *ngIf="sidebarTemplate"> <ng-container *ngTemplateOutlet="sidebarTemplate;context:sidebarTemplateContext"></ng-container> @@ -133,7 +133,7 @@ </ng-container> <ng-container *ngSwitchCase="'custom'"> - <span *ngFor="let extensionTemplate of extensionTemplates"> + <span class="adf-viewer-custom-content" *ngFor="let extensionTemplate of extensionTemplates"> <ng-template *ngIf="extensionTemplate.isVisible" [ngTemplateOutlet]="extensionTemplate.template" [ngTemplateOutletContext]="{ urlFileContent: urlFileContent, extension:extension }"> </ng-template> </span> diff --git a/lib/core/viewer/components/viewer.component.scss b/lib/core/viewer/components/viewer.component.scss index 600ed97138..7104892d4e 100644 --- a/lib/core/viewer/components/viewer.component.scss +++ b/lib/core/viewer/components/viewer.component.scss @@ -13,6 +13,24 @@ &__mimeicon { vertical-align: middle; + height: 18px; + width: 18px; + } + + &-toolbar { + .mat-toolbar { + background-color: mat-color($primary, default-contrast) + } + } + + &__display-name { + font-size: 16px; + opacity: 0.87; + line-height: 1.5; + letter-spacing: -0.4px; + font-weight: normal; + font-style: normal; + font-stretch: normal; } &-container { @@ -22,6 +40,7 @@ overflow-y: auto; overflow-x: hidden; z-index: 1; + background-color: mat-color($background, background); display: flex; flex-direction: row; @@ -70,6 +89,10 @@ justify-content: center; } + &-custom-content { + width: 100vw; + } + &-unknown-content { align-items: center; display: flex; diff --git a/lib/core/viewer/components/viewer.component.spec.ts b/lib/core/viewer/components/viewer.component.spec.ts index 49eb56f376..7a8fa90ffc 100644 --- a/lib/core/viewer/components/viewer.component.spec.ts +++ b/lib/core/viewer/components/viewer.component.spec.ts @@ -208,8 +208,18 @@ describe('ViewerComponent', () => { expect(customElement.querySelector('.adf-viewer-container-more-actions')).toBeDefined(); }); + it('should NOT display sidebar if is not allowed', () => { + component.showSidebar = true; + component.allowSidebar = false; + component.sidebarPosition = 'left'; + fixture.detectChanges(); + let sidebar = element.querySelector('.adf-viewer__sidebar'); + expect(sidebar).toBeNull(); + }); + it('should display sidebar on the left side', () => { component.showSidebar = true; + component.allowSidebar = true; component.sidebarPosition = 'left'; fixture.detectChanges(); let sidebar = element.querySelector('.adf-viewer__sidebar'); @@ -218,6 +228,7 @@ describe('ViewerComponent', () => { it('should display sidebar on the right side', () => { component.showSidebar = true; + component.allowSidebar = true; component.sidebarPosition = 'right'; fixture.detectChanges(); let sidebar = element.querySelector('.adf-viewer__sidebar'); @@ -226,6 +237,7 @@ describe('ViewerComponent', () => { it('should display sidebar on the right side as fallback', () => { component.showSidebar = true; + component.allowSidebar = true; component.sidebarPosition = 'unknown-value'; fixture.detectChanges(); let sidebar = element.querySelector('.adf-viewer__sidebar'); diff --git a/lib/core/viewer/components/viewer.component.ts b/lib/core/viewer/components/viewer.component.ts index 9b49e2365d..72b6ffcd39 100644 --- a/lib/core/viewer/components/viewer.component.ts +++ b/lib/core/viewer/components/viewer.component.ts @@ -54,12 +54,18 @@ export class ViewerComponent implements OnChanges { @Input() urlFile = ''; + @Input() + urlFileViewer: string = null; + @Input() blobFile: Blob; @Input() fileNodeId: string = null; + @Input() + sharedLinkId: string = null; + @Input() overlayMode = false; @@ -96,6 +102,15 @@ export class ViewerComponent implements OnChanges { @Input() sidebarTemplate: TemplateRef<any> = null; + @Input() + mimeType: string; + + @Input() + fileName: string; + + @Input() + downloadUrl: string = null; + @Output() goBack = new EventEmitter<BaseEvent<any>>(); @@ -115,8 +130,6 @@ export class ViewerComponent implements OnChanges { extensionChange = new EventEmitter<string>(); viewerType = 'unknown'; - downloadUrl: string = null; - fileName = 'document'; isLoading = false; node: MinimalNodeEntryEntity; @@ -125,7 +138,6 @@ export class ViewerComponent implements OnChanges { urlFileContent: string; otherMenu: any; extension: string; - mimeType: string; sidebarTemplateContext: { node: MinimalNodeEntryEntity } = { node: null }; private extensions = { @@ -146,10 +158,14 @@ export class ViewerComponent implements OnChanges { private renditionService: RenditionsService) { } + isSourceDefined(): boolean { + return (this.urlFile || this.blobFile || this.fileNodeId || this.sharedLinkId) ? true : false; + } + ngOnChanges(changes: SimpleChanges) { if (this.showViewer) { - if (!this.urlFile && !this.blobFile && !this.fileNodeId) { - throw new Error('Attribute urlFile or fileNodeId or blobFile is required'); + if (!this.isSourceDefined()) { + throw new Error('A content source attribute value is missing.'); } return new Promise((resolve, reject) => { @@ -176,7 +192,7 @@ export class ViewerComponent implements OnChanges { this.downloadUrl = this.urlFile; this.fileName = this.displayName; - this.viewerType = this.getViewerTypeByExtension(this.extension); + this.viewerType = this.urlFileViewer || this.getViewerTypeByExtension(this.extension); if (this.viewerType === 'unknown') { this.viewerType = this.getViewerTypeByMimeType(this.mimeType); } @@ -203,7 +219,7 @@ export class ViewerComponent implements OnChanges { } if (this.viewerType === 'unknown') { - this.displayAsPdf(data.id); + this.displayNodeRendition(data.id); } else { this.isLoading = false; } @@ -219,11 +235,48 @@ export class ViewerComponent implements OnChanges { this.logService.error('This node does not exist'); } ); + } else if (this.sharedLinkId) { + this.isLoading = true; + + this.apiService.sharedLinksApi.getSharedLink(this.sharedLinkId).then(details => { + this.mimeType = details.entry.content.mimeType; + this.displayName = this.getDisplayName(details.entry.name); + this.extension = this.getFileExtension(details.entry.name); + this.fileName = details.entry.name; + + this.urlFileContent = this.apiService.contentApi.getSharedLinkContentUrl(this.sharedLinkId, false); + this.downloadUrl = this.apiService.contentApi.getSharedLinkContentUrl(this.sharedLinkId, true); + + this.viewerType = this.getViewerTypeByMimeType(this.mimeType); + if (this.viewerType === 'unknown') { + this.viewerType = this.getViewerTypeByExtension(this.extension); + } + + if (this.viewerType === 'unknown') { + this.displaySharedLinkRendition(this.sharedLinkId); + } else { + this.isLoading = false; + } + + this.extensionChange.emit(this.extension); + this.isLoading = false; + resolve(); + }); } }); } } + toggleSidebar() { + this.showSidebar = !this.showSidebar; + if (this.showSidebar && this.fileNodeId) { + this.apiService.getInstance().nodes.getNodeInfo(this.fileNodeId) + .then((data: MinimalNodeEntryEntity) => { + this.sidebarTemplateContext.node = data; + }); + } + } + private getDisplayName(name) { return this.displayName || name; } @@ -328,13 +381,20 @@ export class ViewerComponent implements OnChanges { } /** - * Get the token from the local storage + * Get file extension from the string. + * Supports the URL formats like: + * http://localhost/test.jpg?cache=1000 + * http://localhost/test.jpg#cache=1000 * * @param {string} fileName - file name * @returns {string} file name extension */ getFileExtension(fileName: string): string { - return fileName.split('.').pop().toLowerCase(); + if (fileName) { + const match = fileName.match(/\.([^\./\?\#]+)($|\?|\#)/); + return match ? match[1] : null; + } + return null; } isCustomViewerExtension(extension: string): boolean { @@ -393,40 +453,64 @@ export class ViewerComponent implements OnChanges { } } - private displayAsPdf(nodeId: string) { + private async displayNodeRendition(nodeId: string) { this.isLoading = true; - this.renditionService.getRendition(nodeId, 'pdf').subscribe( - (response) => { - const status = response.entry.status.toString(); + try { + const rendition = await this.apiService.renditionsApi.getRendition(nodeId, 'pdf'); + const status = rendition.entry.status.toString(); - if (status === 'CREATED') { - this.isLoading = false; - this.showPdfRendition(nodeId); - } else if (status === 'NOT_CREATED') { - this.renditionService.convert(nodeId, 'pdf').subscribe({ - complete: () => { - this.isLoading = false; - this.showPdfRendition(nodeId); - }, - error: (error) => { - this.isLoading = false; - } - }); - } else { - this.isLoading = false; + if (status === 'CREATED') { + this.viewerType = 'pdf'; + this.urlFileContent = this.apiService.contentApi.getRenditionUrl(nodeId, 'pdf'); + } else if (status === 'NOT_CREATED') { + try { + await this.renditionService.convert(nodeId, 'pdf').toPromise(); + this.viewerType = 'pdf'; + this.urlFileContent = this.apiService.contentApi.getRenditionUrl(nodeId, 'pdf'); + } catch (error) { + this.logService.error(error); } - }, - (err) => { - this.isLoading = false; } - ); + } catch (error) { + this.logService.error(error); + + try { + const imagePreview = await this.apiService.renditionsApi.getRendition(nodeId, 'imgpreview'); + if (imagePreview.entry.status.toString() === 'CREATED') { + this.viewerType = 'image'; + this.urlFileContent = this.apiService.contentApi.getRenditionUrl(nodeId, 'imgpreview'); + } + } catch (error) { + this.logService.error(error); + } + } + + this.isLoading = false; } - private showPdfRendition(nodeId: string) { - if (nodeId) { - this.viewerType = 'pdf'; - this.urlFileContent = this.renditionService.getRenditionUrl(nodeId, 'pdf'); + private async displaySharedLinkRendition(sharedId: string) { + this.isLoading = true; + + try { + const rendition = await this.apiService.renditionsApi.getSharedLinkRendition(sharedId, 'pdf'); + if (rendition.entry.status.toString() === 'CREATED') { + this.viewerType = 'pdf'; + this.urlFileContent = this.apiService.contentApi.getSharedLinkRenditionUrl(sharedId, 'pdf'); + } + } catch (error) { + this.logService.error(error); + try { + const rendition = await this.apiService.renditionsApi.getSharedLinkRendition(sharedId, 'imgpreview'); + if (rendition.entry.status.toString() === 'CREATED') { + this.viewerType = 'image'; + this.urlFileContent = this.apiService.contentApi.getSharedLinkRenditionUrl(sharedId, 'imgpreview'); + } + } catch (error) { + this.logService.error(error); + } } + + this.isLoading = false; } } diff --git a/lib/insights/analytics-process/components/analytics-report-parameters.component.spec.ts b/lib/insights/analytics-process/components/analytics-report-parameters.component.spec.ts index 9984f57b23..79bfb3f131 100644 --- a/lib/insights/analytics-process/components/analytics-report-parameters.component.spec.ts +++ b/lib/insights/analytics-process/components/analytics-report-parameters.component.spec.ts @@ -25,7 +25,7 @@ import { MaterialModule } from '../../material.module'; declare let jasmine: any; -fdescribe('AnalyticsReportParametersComponent', () => { +describe('AnalyticsReportParametersComponent', () => { let component: AnalyticsReportParametersComponent; let fixture: ComponentFixture<AnalyticsReportParametersComponent>; diff --git a/lib/insights/analytics-process/components/widgets/checkbox/checkbox.widget.ts b/lib/insights/analytics-process/components/widgets/checkbox/checkbox.widget.ts index 071d895cc1..d41b2a5b62 100644 --- a/lib/insights/analytics-process/components/widgets/checkbox/checkbox.widget.ts +++ b/lib/insights/analytics-process/components/widgets/checkbox/checkbox.widget.ts @@ -15,14 +15,13 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-access-missing-member no-input-rename */ -/* tslint:disable:no-access-missing-member */ -import { Component, ElementRef, Input, ViewEncapsulation } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { WidgetComponent } from './../widget.component'; + import { Component, ElementRef, Input, ViewEncapsulation } from '@angular/core'; + import { FormGroup } from '@angular/forms'; + import { WidgetComponent } from './../widget.component'; -@Component({ + @Component({ selector: 'analytics-checkbox-widget', templateUrl: './checkbox.widget.html', encapsulation: ViewEncapsulation.None diff --git a/lib/insights/analytics-process/components/widgets/date-range/date-range.widget.ts b/lib/insights/analytics-process/components/widgets/date-range/date-range.widget.ts index 188253c822..83b17f1fff 100644 --- a/lib/insights/analytics-process/components/widgets/date-range/date-range.widget.ts +++ b/lib/insights/analytics-process/components/widgets/date-range/date-range.widget.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { MOMENT_DATE_FORMATS, MomentDateAdapter, UserPreferencesService } from '@alfresco/adf-core'; import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; diff --git a/lib/insights/analytics-process/components/widgets/dropdown/dropdown.widget.ts b/lib/insights/analytics-process/components/widgets/dropdown/dropdown.widget.ts index 494e3317df..3b22d15b9e 100644 --- a/lib/insights/analytics-process/components/widgets/dropdown/dropdown.widget.ts +++ b/lib/insights/analytics-process/components/widgets/dropdown/dropdown.widget.ts @@ -15,9 +15,8 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-access-missing-member no-input-rename */ -/* tslint:disable::no-access-missing-member */ import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { WidgetComponent } from './../widget.component'; diff --git a/lib/insights/analytics-process/components/widgets/duration/duration.widget.ts b/lib/insights/analytics-process/components/widgets/duration/duration.widget.ts index d6cc715e91..6f45fe3485 100644 --- a/lib/insights/analytics-process/components/widgets/duration/duration.widget.ts +++ b/lib/insights/analytics-process/components/widgets/duration/duration.widget.ts @@ -15,9 +15,8 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-access-missing-member no-input-rename */ -/* tslint:disable::no-access-missing-member */ import { Component, ElementRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { ParameterValueModel } from '../../../../diagram/models/report/parameterValue.model'; diff --git a/lib/insights/analytics-process/components/widgets/number/number.widget.ts b/lib/insights/analytics-process/components/widgets/number/number.widget.ts index b56a9adb20..dc05a57f76 100644 --- a/lib/insights/analytics-process/components/widgets/number/number.widget.ts +++ b/lib/insights/analytics-process/components/widgets/number/number.widget.ts @@ -15,9 +15,8 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ + /* tslint:disable:component-selector no-access-missing-member no-input-rename */ -/* tslint:disable::no-access-missing-member */ import { Component, ElementRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { FormGroup, Validators } from '@angular/forms'; import { WidgetComponent } from './../widget.component'; diff --git a/lib/insights/diagram/components/diagram.component.boundary.spec.ts b/lib/insights/diagram/components/diagram.component.boundary.spec.ts index 4602c0a8e5..a84bedad43 100644 --- a/lib/insights/diagram/components/diagram.component.boundary.spec.ts +++ b/lib/insights/diagram/components/diagram.component.boundary.spec.ts @@ -31,9 +31,7 @@ describe('Diagrams boundary', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - DiagramsModule - ] + imports: [DiagramsModule] }).compileComponents(); })); diff --git a/lib/insights/diagram/components/diagram.component.events.spec.ts b/lib/insights/diagram/components/diagram.component.events.spec.ts index 07a24731eb..3811838ed0 100644 --- a/lib/insights/diagram/components/diagram.component.events.spec.ts +++ b/lib/insights/diagram/components/diagram.component.events.spec.ts @@ -31,9 +31,7 @@ describe('Diagrams events', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - DiagramsModule - ] + imports: [DiagramsModule] }).compileComponents(); })); diff --git a/lib/insights/diagram/components/diagram.component.swim.spec.ts b/lib/insights/diagram/components/diagram.component.swim.spec.ts index 132bb50170..706d2d88fa 100644 --- a/lib/insights/diagram/components/diagram.component.swim.spec.ts +++ b/lib/insights/diagram/components/diagram.component.swim.spec.ts @@ -31,11 +31,7 @@ describe('Diagrams swim', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [ - DiagramsModule - ], - declarations: [ - ] + imports: [DiagramsModule] }).compileComponents(); })); diff --git a/lib/insights/diagram/components/diagram.component.throw.spec.ts b/lib/insights/diagram/components/diagram.component.throw.spec.ts index a9522f745f..256a6eef0f 100644 --- a/lib/insights/diagram/components/diagram.component.throw.spec.ts +++ b/lib/insights/diagram/components/diagram.component.throw.spec.ts @@ -31,36 +31,27 @@ describe('Diagrams throw', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [], - declarations: [ - DiagramsModule - ], - providers: [ + imports: [DiagramsModule] + }).compileComponents().then(() => { + jasmine.Ajax.install(); - ] - }).compileComponents(); + fixture = TestBed.createComponent(DiagramComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.processInstanceId = '38399'; + component.processDefinitionId = 'fakeprocess:24:38399'; + component.metricPercentages = { startEvent: 0 }; + + fixture.detectChanges(); + }); })); - beforeEach(() => { - fixture = TestBed.createComponent(DiagramComponent); - component = fixture.componentInstance; - element = fixture.nativeElement; - fixture.detectChanges(); - }); - - beforeEach(() => { - jasmine.Ajax.install(); - component.processInstanceId = '38399'; - component.processDefinitionId = 'fakeprocess:24:38399'; - component.metricPercentages = { startEvent: 0 }; - }); - afterEach(() => { component.success.unsubscribe(); jasmine.Ajax.uninstall(); }); - let ajaxReply = (resp: any) => { + let ajaxReply = (resp: any) => { jasmine.Ajax.requests.mostRecent().respondWith({ status: 200, contentType: 'json', diff --git a/lib/insights/diagram/services/diagrams.service.spec.ts b/lib/insights/diagram/services/diagrams.service.spec.ts new file mode 100644 index 0000000000..1297cd5dc6 --- /dev/null +++ b/lib/insights/diagram/services/diagrams.service.spec.ts @@ -0,0 +1,84 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, TestBed } from '@angular/core/testing'; +import { AppConfigService, CoreModule } from '@alfresco/adf-core'; +import { DiagramsService } from './diagrams.service'; + +declare let jasmine: any; + +describe('DiagramsService', () => { + + let service: DiagramsService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CoreModule + ], + providers: [ + DiagramsService + ] + }).compileComponents(); + })); + + beforeEach(() => { + let appConfig: AppConfigService = TestBed.get(AppConfigService); + appConfig.config.ecmHost = 'http://localhost:9876/ecm'; + + service = TestBed.get(DiagramsService); + }); + + beforeEach(() => { + jasmine.Ajax.install(); + }); + + afterEach(() => { + jasmine.Ajax.uninstall(); + }); + + it('getProcessDefinitionModel should perform a call against the server', (done) => { + service.getProcessDefinitionModel('fake-processDefinitionId').subscribe(() => { + done(); + }); + + jasmine.Ajax.requests.mostRecent().respondWith({ + status: 200 + }); + }); + + it('getRunningProcessDefinitionModel should perform a call against the server', (done) => { + service.getRunningProcessDefinitionModel('fake-processInstanceId').subscribe(() => { + done(); + }); + + jasmine.Ajax.requests.mostRecent().respondWith({ + status: 200 + }); + }); + + it('getTagsByNodeId catch errors call', (done) => { + service.getProcessDefinitionModel('fake-processDefinitionId').subscribe(() => { + }, () => { + done(); + }); + + jasmine.Ajax.requests.mostRecent().respondWith({ + status: 403 + }); + }); +}); diff --git a/lib/insights/index.ts b/lib/insights/index.ts index 42a4a5bc7b..e9b19ef037 100644 --- a/lib/insights/index.ts +++ b/lib/insights/index.ts @@ -16,7 +16,9 @@ */ export * from './insights.module'; -// + export * from './analytics-process/analytics-process.module'; -// +export * from './diagram/diagram.module'; + export * from './analytics-process'; +export * from './diagram'; diff --git a/lib/insights/insights.module.ts b/lib/insights/insights.module.ts index 82a49983b5..b144c046c9 100644 --- a/lib/insights/insights.module.ts +++ b/lib/insights/insights.module.ts @@ -53,7 +53,6 @@ import { MaterialModule } from './material.module'; FormsModule, ReactiveFormsModule, BrowserAnimationsModule, - MaterialModule, DiagramsModule, AnalyticsProcessModule ] diff --git a/lib/insights/package.json b/lib/insights/package.json index e4e2bf535f..f0716fe403 100644 --- a/lib/insights/package.json +++ b/lib/insights/package.json @@ -1,7 +1,7 @@ { "name": "@alfresco/adf-insights", "description": "Alfresco ADF insights", - "version": "2.0.1", + "version": "2.1.0", "author": "Alfresco Software, Ltd.", "main": "bundles/adf-insights.js", "repository": { @@ -12,22 +12,22 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "@angular/animations": "5.0.0", - "@angular/cdk": "5.0.0-rc0", - "@angular/common": "5.0.0", - "@angular/compiler": "5.0.0", - "@angular/core": "5.0.0", - "@angular/flex-layout": "2.0.0-beta.10", - "@angular/forms": "5.0.0", - "@angular/http": "5.0.0", - "@angular/material": "5.0.0-rc0", - "@angular/material-moment-adapter": "^5.0.0-rc0", - "@angular/platform-browser": "5.0.0", - "@angular/platform-browser-dynamic": "5.0.0", - "@angular/router": "5.0.0", + "@angular/animations": "5.1.1", + "@angular/cdk": "5.0.1", + "@angular/common": "5.1.1", + "@angular/compiler": "5.1.1", + "@angular/core": "5.1.1", + "@angular/flex-layout": "2.0.0-beta.12", + "@angular/forms": "5.1.1", + "@angular/http": "5.1.1", + "@angular/material": "5.0.1", + "@angular/material-moment-adapter": "5.0.1", + "@angular/platform-browser": "5.1.1", + "@angular/platform-browser-dynamic": "5.1.1", + "@angular/router": "5.1.1", "@ngx-translate/core": "8.0.0", - "alfresco-js-api": "2.0.0", - "@alfresco/adf-core": "2.0.0", + "alfresco-js-api": "2.1.0", + "@alfresco/adf-core": "2.1.0", "chart.js": "2.5.0", "core-js": "2.4.1", "hammerjs": "2.0.8", diff --git a/lib/ng-package/ng-package-core.json b/lib/ng-package/ng-package-core.json index 3320d01be5..a8c7d8a51f 100644 --- a/lib/ng-package/ng-package-core.json +++ b/lib/ng-package/ng-package-core.json @@ -11,6 +11,8 @@ "minimatch": "minimatch", "@angular/platform-browser/animations": "@angular/platform-browser/animations", "@angular/material": "@angular/material", + "@mat-datetimepicker/core": "@mat-datetimepicker/core", + "@mat-datetimepicker/moment": "@mat-datetimepicker/moment", "@angular/flex-layout": "@angular/flex-layout", "@angular/material-moment-adapter": "@angular/material-moment-adapter", "@angular/animations": "@angular/animations", diff --git a/lib/ng-package/ng-package-process.json b/lib/ng-package/ng-package-process.json index cbc383f54f..52783e606a 100644 --- a/lib/ng-package/ng-package-process.json +++ b/lib/ng-package/ng-package-process.json @@ -13,6 +13,7 @@ "@angular/material": "@angular/material", "@angular/flex-layout": "@angular/flex-layout", "@alfresco/adf-core": "@alfresco/adf-core", + "@alfresco/adf-content-services": "@alfresco/adf-content-services", "@angular/material-moment-adapter": "@angular/material-moment-adapter", "@angular/animations": "@angular/animations", "@angular/cdk/platform": "@angular/cdk/platform", diff --git a/lib/package.json b/lib/package.json index 17bf8dd1c6..edebf8c121 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,7 +1,7 @@ { "name": "alfresco-components", "description": "Alfresco Angular components", - "version": "2.0.0", + "version": "2.1.0", "author": "Alfresco Software, Ltd.", "scripts": { "clean": "rimraf node_modules", @@ -20,12 +20,13 @@ "bundlesize-check": "bundlesize", "markdownlint": "markdownlint ../docs", "doc": "npm run toc && npm run markdownlint && npm run webpack -- --config config/webpack.doc.js --progress --profile --bail", - "docindex": "node config/buildFullDocIndex.js", - "mv-es5": "mv core/dist/@alfresco core/dist/esm5 && mv content-services/dist/@alfresco content-services/dist/esm5 && mv process-services/dist/@alfresco process-services/dist/esm5 && mv insights/dist/@alfresco insights/dist/esm5 ", + "docindex": "node config/DocProcessor/docProcessor.js ../docs", "copy-i18n": "mkdir -p core/dist/bundles/assets/adf-core/i18n && cp -R core/i18n/* core/dist/bundles/assets/adf-core/i18n && mkdir -p content-services/dist/bundles/assets/adf-content-services/i18n && cp -R content-services/i18n/* content-services/dist/bundles/assets/adf-content-services/i18n && mkdir -p process-services/dist/bundles/assets/adf-process-services/i18n && cp -R process-services/i18n/* process-services/dist/bundles/assets/adf-process-services/i18n && mkdir -p insights/dist/bundles/assets/adf-insights/i18n && cp -R insights/i18n/* insights/dist/bundles/assets/adf-insights/i18n", "copy-assets": "cp -R core/assets/* core/dist/bundles/assets && cp -R content-services/assets/* content-services/dist/bundles/assets && cp -R process-services/assets/* process-services/dist/bundles/assets", - "new-build": "npm run build-bundles && npm run build-style-new && npm run copy-i18n && npm run copy-assets && npm run mv-es5", - "build-bundles": "npm run build-core && mkdir -p ./node_modules/@alfresco/adf-core/ && cp -R ./core/dist/* ./node_modules/@alfresco/adf-core/ && npm run build-content && npm run build-process && npm run build-insights", + "new-build": "npm run build-bundles && npm run build-style-new && npm run copy-i18n && npm run copy-assets", + "build-bundles": "npm run build-core && npm run link-core && npm run build-content && npm run link-content && npm run build-process && npm run build-insights", + "link-core": "mkdir -p ./node_modules/@alfresco/adf-core/ && cp -R ./core/dist/* ./node_modules/@alfresco/adf-core/", + "link-content": "mkdir -p ./node_modules/@alfresco/adf-content-services/ && cp -R ./content-services/dist/* ./node_modules/@alfresco/adf-content-services/", "build-core": "ng-packagr -p ./ng-package/ng-package-core.json", "build-content": "ng-packagr -p ./ng-package/ng-package-content.json", "build-process": "ng-packagr -p ./ng-package/ng-package-process.json", @@ -50,28 +51,30 @@ "process services" ], "dependencies": { - "@angular/animations": "5.0.0", - "@angular/cdk": "5.0.0-rc0", - "@angular/common": "5.0.0", - "@angular/compiler": "5.0.0", - "@angular/core": "5.0.0", - "@angular/flex-layout": "2.0.0-beta.10", - "@angular/forms": "5.0.0", - "@angular/http": "5.0.0", - "@angular/material": "5.0.0-rc0", - "@angular/material-moment-adapter": "^5.0.0-rc0", - "@angular/platform-browser": "5.0.0", - "@angular/platform-browser-dynamic": "5.0.0", - "@angular/router": "5.0.0", + "@angular/animations": "5.1.1", + "@angular/cdk": "5.0.1", + "@angular/common": "5.1.1", + "@angular/compiler": "5.1.1", + "@angular/core": "5.1.1", + "@angular/flex-layout": "2.0.0-beta.12", + "@angular/forms": "5.1.1", + "@angular/http": "5.1.1", + "@angular/material": "5.0.1", + "@angular/material-moment-adapter": "5.0.1", + "@angular/platform-browser": "5.1.1", + "@angular/platform-browser-dynamic": "5.1.1", + "@angular/router": "5.1.1", + "@mat-datetimepicker/core": "^1.0.1", + "@mat-datetimepicker/moment": "^1.0.1", "@ngx-translate/core": "8.0.0", - "alfresco-js-api": "2.0.0", + "alfresco-js-api": "2.1.0", "chart.js": "2.5.0", "core-js": "2.4.1", "hammerjs": "2.0.8", "minimatch": "3.0.4", "moment": "2.18.1", "moment-es6": "^1.0.0", - "ng-packagr": "^1.6.0", + "ng-packagr": "2.0.0-rc.4", "ng2-charts": "1.6.0", "pdfjs-dist": "1.5.404", "raphael": "2.2.7", @@ -81,6 +84,7 @@ "zone.js": "0.8.14" }, "devDependencies": { + "@angular/compiler-cli": "5.1.1", "@types/hammerjs": "2.0.35", "@types/jasmine": "^2.5.35", "@types/node": "6.0.90", @@ -100,7 +104,7 @@ "happypack": "4.0.0", "html-loader": "0.4.4", "html-webpack-plugin": "2.28.0", - "istanbul-instrumenter-loader": "0.2.0", + "istanbul-instrumenter-loader": "3.0.0", "jasmine-ajax": "3.2.0", "jasmine-core": "2.4.1", "karma": "0.13.22", @@ -113,7 +117,7 @@ "karma-remap-istanbul": "0.6.0", "karma-sourcemap-loader": "0.3.7", "karma-systemjs": "0.16.0", - "karma-webpack": "2.0.5", + "karma-webpack": "2.0.9", "loader-utils": "1.1.0", "markdown-toc": "1.1.0", "markdownlint-cli": "^0.3.1", @@ -126,23 +130,22 @@ "run-sequence": "1.2.2", "sass-loader": "6.0.5", "script-loader": "0.7.0", - "scss-bundle": "^2.1.0", - "source-map-loader": "0.1.6", + "scss-bundle": "2.1.0", + "source-map-loader": "0.2.3", "style-loader": "0.13.1", "systemjs-builder": "0.15.34", "to-string-loader": "1.1.5", "traceur": "0.0.91", - "ts-loader": "3.1.1", + "ts-loader": "3.3.0", "ts-node": "2.0.0", "tslint": "5.7.0", "tslint-loader": "3.5.3", "typescript": "2.6.1", - "uglifyjs-webpack-plugin": "^1.0.1", - "webpack": "3.8.1", + "uglifyjs-webpack-plugin": "1.1.6", + "webpack": "3.10.0", "webpack-bundle-analyzer": "2.9.0", - "webpack-dev-server": "2.9.4", - "webpack-merge": "2.6.1", - "wsrv": "0.1.7" + "webpack-dev-server": "2.11.1", + "webpack-merge": "2.6.1" }, "license": "Apache-2.0", "bundlesize": [ diff --git a/lib/process-services/app-list/apps-list.component.html b/lib/process-services/app-list/apps-list.component.html index 498ce131f0..dab36c7336 100644 --- a/lib/process-services/app-list/apps-list.component.html +++ b/lib/process-services/app-list/apps-list.component.html @@ -24,7 +24,7 @@ <h1>{{getAppName(app) | async}}</h1> </div> <mat-card-subtitle class="adf-app-listgrid-item-card-subtitle" fxFlex="1 0 auto"> - <p>{{app.description}}</p> + <div class="line-clamp">{{app.description}}</div> </mat-card-subtitle> <mat-card-actions class="adf-app-listgrid-item-card-actions"> diff --git a/lib/process-services/app-list/apps-list.component.scss b/lib/process-services/app-list/apps-list.component.scss index 682a1ab76b..2af14a42ee 100644 --- a/lib/process-services/app-list/apps-list.component.scss +++ b/lib/process-services/app-list/apps-list.component.scss @@ -1,3 +1,8 @@ +// TODO: import this mixin from core mixins... +// ------- +@mixin line-clamp($line-height: 1.25, $lines: 3) { display: block; display: -webkit-box; -webkit-box-orient: vertical; position: relative; line-height: $line-height; overflow: hidden; text-overflow: ellipsis; padding: 0 !important; -webkit-line-clamp: $lines; height: calc(0.99em * #{$line-height} * #{$lines}); &:after { content: '...'; text-align: right; bottom: 0; right: 0; width: 25%; display: block; position: absolute; height: calc(0.99em * #{$line-height}); } } @supports (-webkit-line-clamp: 1) { .line-clamp:after { display: none !important; } } +// ------- + :host { width: 100%; } @@ -52,6 +57,7 @@ $themes: ( right: 20px; top: 20px; padding: 16px; + z-index: 9998; &-icon { font-size: 70px; @@ -69,6 +75,8 @@ $themes: ( &-title { padding: 16px; + margin-bottom: 0; + z-index: 9999; h1 { color: white; @@ -83,7 +91,13 @@ $themes: ( } &-subtitle { + color: white; + z-index: 9999; padding: 16px; + + .line-clamp { + @include line-clamp(1.25, 3); + } } &-actions { diff --git a/lib/process-services/app-list/apps-list.component.spec.ts b/lib/process-services/app-list/apps-list.component.spec.ts index f3ec7ca70b..63f7296dd4 100644 --- a/lib/process-services/app-list/apps-list.component.spec.ts +++ b/lib/process-services/app-list/apps-list.component.spec.ts @@ -55,7 +55,7 @@ describe('AppsListComponent', () => { component = fixture.componentInstance; debugElement = fixture.debugElement; - service = fixture.debugElement.injector.get(AppsProcessService); + service = TestBed.get(AppsProcessService); getAppsSpy = spyOn(service, 'getDeployedApplications').and.returnValue(Observable.of(deployedApps)); }); diff --git a/lib/process-services/app-list/apps-list.component.ts b/lib/process-services/app-list/apps-list.component.ts index 01bbb8a786..fb3e83aec6 100644 --- a/lib/process-services/app-list/apps-list.component.ts +++ b/lib/process-services/app-list/apps-list.component.ts @@ -37,15 +37,21 @@ export class AppsListComponent implements OnInit { public static DEFAULT_TASKS_APP_ICON: string = 'glyphicon-asterisk'; public static DEFAULT_TASKS_APP_MATERIAL_ICON: string = 'favorite_border'; + /** (**required**) Defines the layout of the apps. There are two possible + * values, "GRID" and "LIST". + */ @Input() layoutType: string = AppsListComponent.LAYOUT_GRID; + /** Provides a way to filter the apps to show. */ @Input() filtersAppId: any[]; + /** Emitted when an app entry is clicked. */ @Output() appClick: EventEmitter<AppDefinitionRepresentationModel> = new EventEmitter<AppDefinitionRepresentationModel>(); + /** Emitted when an error occurs. */ @Output() error: EventEmitter<any> = new EventEmitter<any>(); diff --git a/lib/process-services/app-list/apps-list.module.ts b/lib/process-services/app-list/apps-list.module.ts index 26a8dfdc28..d6423d50d6 100644 --- a/lib/process-services/app-list/apps-list.module.ts +++ b/lib/process-services/app-list/apps-list.module.ts @@ -22,6 +22,7 @@ import { MaterialModule } from '../material.module'; import { TranslateModule } from '@ngx-translate/core'; import { AppsListComponent } from './apps-list.component'; +import { SelectAppsDialogComponent } from './select-apps-dialog-component'; @NgModule({ imports: [ @@ -31,11 +32,16 @@ import { AppsListComponent } from './apps-list.component'; TranslateModule ], declarations: [ - AppsListComponent + AppsListComponent, + SelectAppsDialogComponent ], providers: [], exports: [ - AppsListComponent + AppsListComponent, + SelectAppsDialogComponent + ], + entryComponents: [ + SelectAppsDialogComponent ] }) export class AppsListModule { diff --git a/lib/process-services/app-list/public-api.ts b/lib/process-services/app-list/public-api.ts index fb2c71a74d..48eb0254f0 100644 --- a/lib/process-services/app-list/public-api.ts +++ b/lib/process-services/app-list/public-api.ts @@ -16,3 +16,4 @@ */ export * from './apps-list.component'; +export * from './select-apps-dialog-component'; diff --git a/lib/process-services/app-list/select-apps-dialog-component.html b/lib/process-services/app-list/select-apps-dialog-component.html new file mode 100644 index 0000000000..e49a1c08f6 --- /dev/null +++ b/lib/process-services/app-list/select-apps-dialog-component.html @@ -0,0 +1,13 @@ +<header mat-dialog-title id="adf-selet-app-dialog-title">{{'APP.DIALOG.TITLE' | translate}}</header> +<section mat-dialog-content> + + <mat-select id="adf-selet-app-dialog-dropdown" placeholder="{{'APP.DIALOG.LIST' | translate}}" [(value)]="selectedProcess" > + <mat-option *ngFor="let currentProcessApp of processApps" [value]="currentProcessApp"> + {{ currentProcessApp.name }} + </mat-option> + </mat-select> + +</section> +<footer mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> + <button mat-button (click)="onStart()">{{'APP.DIALOG.START' | translate}}</button> +</footer> diff --git a/lib/process-services/app-list/select-apps-dialog-component.spec.ts b/lib/process-services/app-list/select-apps-dialog-component.spec.ts new file mode 100644 index 0000000000..3e236b10dc --- /dev/null +++ b/lib/process-services/app-list/select-apps-dialog-component.spec.ts @@ -0,0 +1,131 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { Component } from '@angular/core'; +import { async, TestBed } from '@angular/core/testing'; +import { ComponentFixture } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { MaterialModule } from '../material.module'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { MatDialog } from '@angular/material'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { AppsProcessService } from '@alfresco/adf-core'; +import { deployedApps } from '../mock/apps-list.mock'; +import { Observable } from 'rxjs/Observable'; + +import { SelectAppsDialogComponent } from './select-apps-dialog-component'; + +@Component({ + selector: 'adf-dialog-test', + template: '' +}) +export class DialogSelectAppTestComponent { + + processId: any; + + dialogRef: any; + + constructor(private dialog: MatDialog) { + } + + startProcesAction() { + this.dialogRef = this.dialog.open(SelectAppsDialogComponent, { + width: '630px' + }); + + this.dialogRef.afterClosed().subscribe(selectedProcess => { + this.processId = selectedProcess.id; + }); + } + +} + +describe('Select app dialog', () => { + + let fixture: ComponentFixture<DialogSelectAppTestComponent>; + let component: DialogSelectAppTestComponent; + let dialogRef; + let overlayContainerElement: HTMLElement; + let service: AppsProcessService; + + beforeEach(async(() => { + dialogRef = { + close: jasmine.createSpy('close') + }; + + TestBed.configureTestingModule({ + imports: [ + MaterialModule, + FormsModule, + ReactiveFormsModule, + BrowserDynamicTestingModule + ], + declarations: [ + SelectAppsDialogComponent, + DialogSelectAppTestComponent + ], + providers: [ + AppsProcessService, + { + provide: OverlayContainer, + useFactory: () => { + overlayContainerElement = document.createElement('div'); + return { getContainerElement: () => overlayContainerElement }; + } + } + { + provide: MatDialogRef, useValue: dialogRef + }, + { + provide: MAT_DIALOG_DATA, + useValue: {} + } + ] + }); + + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { entryComponents: [SelectAppsDialogComponent] } + }); + + TestBed.compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogSelectAppTestComponent); + component = fixture.componentInstance; + + service = TestBed.get(AppsProcessService); + + spyOn(service, 'getDeployedApplications').and.returnValue(Observable.of(deployedApps)); + }); + + describe('Dialog', () => { + + beforeEach(() => { + fixture.detectChanges(); + }; + + it('should init title and dropdown', () => { + component.startProcesAction(); + + expect(overlayContainerElement.querySelector('.adf-selet-app-dialog-title')).toBeDefined(); + expect(overlayContainerElement.querySelector('.adf-selet-app-dialog-dropdown')).toBeDefined(); + }); + }); + +}); diff --git a/lib/process-services/app-list/select-apps-dialog-component.ts b/lib/process-services/app-list/select-apps-dialog-component.ts new file mode 100644 index 0000000000..953de62b17 --- /dev/null +++ b/lib/process-services/app-list/select-apps-dialog-component.ts @@ -0,0 +1,56 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { AppsProcessService, NotificationService, TranslationService } from '@alfresco/adf-core'; +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'adf-select-apps-dialog', + templateUrl: 'select-apps-dialog-component.html' +}) +export class SelectAppsDialogComponent { + + processApps: any; + + selectedProcess: any; + + constructor(private appsProcessService: AppsProcessService, + private translateService: TranslationService, + private notificationService: NotificationService, + public dialogRef: MatDialogRef<SelectAppsDialogComponent>, + @Inject(MAT_DIALOG_DATA) public data: any) { + + this.appsProcessService.getDeployedApplications().subscribe( + (apps: any[]) => { + this.processApps = apps.filter((currentApp) => { + return currentApp.id; + }); + }, + (err) => { + this.translateService.get('TAG.MESSAGES.EXIST').subscribe((error) => { + this.notificationService.openSnackMessage(error, 4000); + }); + } + ); + + } + + onStart(): void { + this.dialogRef.close(this.selectedProcess); + } +} diff --git a/lib/process-services/assets/images/alfresco-flower.svg b/lib/process-services/assets/images/alfresco-flower.svg new file mode 100644 index 0000000000..0b796b7d60 --- /dev/null +++ b/lib/process-services/assets/images/alfresco-flower.svg @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="17.2 134.6 562 573.1" enable-background="new 17.2 134.6 562 573.1" xml:space="preserve"> +<path fill="#FFFFFF" d="M298.2,421.3L219,340.4l-2.9-2.9c-45.4-46.5-119.5-46.5-164.7-0.2c-45.6,46.3-45.6,121.5,0,167.8 + c45.2,46.4,119,46.4,164.4,0L298.2,421.3z"/> +<path fill="#FFFFFF" d="M298.2,421.2v114.3l0.2,4.2c0,65.5-52.2,119-116.5,119c-64.3,0-116.5-53.1-116.5-118.6 + c0-65.6,52.4-118.8,116.5-118.8H298.2z"/> +<path fill="#FFFFFF" d="M298.2,421.2v114.3l0.2,4.2c0,65.5-52.2,119-116.5,119c-64.3,0-116.5-53.1-116.5-118.6 + c0-65.6,52.4-118.8,116.5-118.8H298.2z"/> +<path fill="#FFFFFF" d="M298.2,421.2v114.3l0.2,4.2c0,65.5-52.2,119-116.5,119c-64.3,0-116.5-53.1-116.5-118.6 + c0-7.3,0.8-14.4,2-21.2c45.4,32.2,108.2,27.7,148.6-13.7l82.4-83.8V421.2z"/> +<path fill="#8ABF42" d="M181.9,436.1c-56.1,0-101.6,46.5-101.6,103.8c0,57.1,45.6,103.5,101.6,103.5c56.2,0,101.7-46.4,101.7-103.5 + v-4.5v-99.3H182.1H181.9z"/> +<path fill="#FFFFFF" d="M298.2,421.2l79.2,80.8l3.3,2.9c45.2,46.4,45.5,121.8,0,168.1c-45.6,46.3-119.2,46.3-164.8,0 + c-45.4-46.3-45.4-121.5,0-168L298.2,421.2z"/> +<path fill="#8ABF42" d="M226.5,515.6c-39.8,40.5-39.8,106.2,0,146.6c39.6,40.5,104,40.5,143.8,0c39.6-40.4,39.6-106.1,0-146.6 + l-3.3-3l-68.7-70.2l-71.7,73.1V515.6z"/> +<path fill="#FFFFFF" d="M298.2,421.2h112.2l4.1-0.2c64.4,0,116.5,53.2,116.5,118.8c0,65.6-52,118.6-116.3,118.6 + c-64.3,0-116.5-53.1-116.5-118.6V421.2z"/> +<path fill="#8ABF42" d="M313.1,539.8c0,57.3,45.4,103.7,101.6,103.7c56,0,101.4-46.4,101.4-103.7c0-57.3-45.4-103.6-101.4-103.6 + h-4.3h-97.4v103.3V539.8z"/> +<path fill="#FFFFFF" d="M298.2,421.2l79.2-80.9l3.1-3.1c45.4-46.3,119.2-46.5,164.6-0.2c45.4,46.4,45.4,121.5,0,167.9 + c-45.4,46.4-119.2,46.4-164.6,0L298.2,421.2z"/> +<path fill="#8ABF42" d="M391.1,494.3c39.4,40.6,103.8,40.6,143.6,0c39.8-40.4,39.8-106,0-146.5c-39.8-40.5-104.1-40.5-143.6,0 + l-3.3,3.2l-68.6,70.3l71.5,73L391.1,494.3z"/> +<path fill="#FFFFFF" d="M298.2,421.2V306.8v-4.3c0-65.5,51.9-118.8,116.5-118.8c64.2,0,116.1,52.9,116.1,118.6 + c0,65.6-52,118.8-116.1,118.8H298.2z"/> +<path fill="#F89F31" d="M414.7,406.1c56,0,101.4-46.4,101.4-103.7c0-57.1-45.4-103.6-101.4-103.6c-56.3,0-101.8,46.5-101.8,103.6 + l0.2,4.4v99.3h101.1H414.7z"/> +<path fill="#FFFFFF" d="M298.2,421.2L219,340.3l-3.1-2.9c-45.4-46.5-45.4-121.8,0-168.1c45.2-46.4,119.1-46.4,164.6,0 + c45.4,46.3,45.4,121.6,0,167.9L298.2,421.2z"/> +<path fill="#0080C5" d="M370.1,327.6c39.6-40.5,39.6-106.2,0-146.6c-39.8-40.5-104-40.5-143.7,0c-39.8,40.4-39.8,106.1,0,146.6 + l2.9,3.1l69,70.1l71.6-73L370.1,327.6z"/> +<path fill="#FFFFFF" d="M298.2,422.1H186.1l-4.2,0.1c-64.1,0-116.5-53.2-116.5-118.8c0-65.5,52.2-118.7,116.3-118.7 + c64.3,0,116.6,53.2,116.6,118.8V422.1z"/> +<path fill="#0080C5" d="M283.5,303.5c0-57.3-45.4-103.6-101.8-103.6c-56,0-101.6,46.3-101.6,103.5c0,57.2,45.6,103.7,101.6,103.7 + l4.4-0.1h97.4V303.8V303.5z"/> +<path fill="#FFFFFF" d="M298.2,422.1l-281,0.1c0-30.4,11.2-60.8,34.2-84c45.2-46.3,119-46.3,164.4,0L298.2,422.1z"/> +<path fill="#0080C5" d="M205.4,348.9l71.9,73.3h-95.5c-51.6,0-95.5,34.3-110.7,81.9c-3.1-2.7-6.4-5.5-9.3-8.7 + c-39.8-40.5-39.8-106,0-146.5C101.3,308.4,165.8,308.4,205.4,348.9"/> +<path fill="#005DA9" d="M205.6,349l56.9,58l-80.9,0.2c-46.6,0-85.8-31.9-97.8-75.5c38.6-22.1,89.2-16.8,121.6,17.1L205.6,349z"/> +<path fill="#005DA9" d="M283.5,303.7l-0.1,82.1l-57.1-58.2c-32.9-33.6-38.7-84.4-17-123.8c42.7,12.2,74.2,52.2,74.2,99.8V303.7z"/> +<path fill="#005DA9" d="M312.6,386v-82.4c0-47.5,31.5-87.5,74.2-99.8c21.6,39.5,17.6,89-16.4,123.3L312.6,386z"/> +<path fill="#FFDE4F" d="M333.9,406.1l57.1-58.3c32.9-33.6,82.8-39.3,121.4-17.2c-12.2,43.5-49.4,74.8-95.3,75.6L333.9,406.1z"/> +<path fill="#48A64A" d="M333.8,436.1h80.8c46.6,0,85.8,31.7,97.8,75.4c-38.9,22.2-88.7,16.3-121.8-17.2L333.8,436.1z"/> +<path fill="#48A64A" d="M313.1,539.2v-82l57.2,58.2c32.9,33.7,38.5,84.7,16.8,124.1c-42.7-12.2-74-52.4-74-99.9V539.2z"/> +<path fill="#48A64A" d="M226.6,515.4l56.9-58l0.1,82.4c0,47.5-31.3,87.4-74,99.7c-21.6-39.5-15.8-90.4,17-123.9V515.4z"/> +<path fill="#48A64A" d="M182.4,436.1h80.4l-57,58.2c-32.8,33.5-83.1,39.3-121.7,17.3c12.1-43.7,51.4-75.5,98-75.5H182.4z"/> +</svg> diff --git a/lib/process-services/attachment/create-process-attachment.component.ts b/lib/process-services/attachment/create-process-attachment.component.ts index 669e137f05..c06bbb4b69 100644 --- a/lib/process-services/attachment/create-process-attachment.component.ts +++ b/lib/process-services/attachment/create-process-attachment.component.ts @@ -25,12 +25,19 @@ import { ProcessContentService } from '@alfresco/adf-core'; }) export class CreateProcessAttachmentComponent implements OnChanges { + /** (required) The ID of the process instance to display. */ @Input() processInstanceId: string; + /** Emitted when an error occurs while creating or uploading an attachment + * from the user within the component. + */ @Output() error: EventEmitter<any> = new EventEmitter<any>(); + /** Emitted when an attachment is successfully created or uploaded + * from within the component. + */ @Output() success: EventEmitter<any> = new EventEmitter<any>(); diff --git a/lib/process-services/attachment/create-task-attachment.component.ts b/lib/process-services/attachment/create-task-attachment.component.ts index 65310420e3..037136c9d5 100644 --- a/lib/process-services/attachment/create-task-attachment.component.ts +++ b/lib/process-services/attachment/create-task-attachment.component.ts @@ -25,12 +25,19 @@ import { ProcessContentService } from '@alfresco/adf-core'; }) export class AttachmentComponent implements OnChanges { + /** (required) The numeric ID of the task to display. */ @Input() taskId: string; + /** Emitted when an error occurs while creating or uploading an + * attachment from the user within the component. + */ @Output() error: EventEmitter<any> = new EventEmitter<any>(); + /** Emitted when an attachment is created or uploaded successfully + * from within the component. + */ @Output() success: EventEmitter<any> = new EventEmitter<any>(); diff --git a/lib/process-services/attachment/process-attachment-list.component.html b/lib/process-services/attachment/process-attachment-list.component.html index 456d650759..f2fe9bf97b 100644 --- a/lib/process-services/attachment/process-attachment-list.component.html +++ b/lib/process-services/attachment/process-attachment-list.component.html @@ -1,16 +1,22 @@ -<adf-datatable [rows]="attachments" [actions]="true" [loading]="isLoading" (rowDblClick)="openContent($event)" (showRowActionsMenu)="onShowRowActionsMenu($event)" - (executeRowAction)="onExecuteRowAction($event)"> +<adf-datatable [rows]="attachments" + [actions]="true" + [loading]="isLoading" + (rowDblClick)="openContent($event)" + (showRowActionsMenu)="onShowRowActionsMenu($event)" + (executeRowAction)="onExecuteRowAction($event)"> - <adf-empty-list *ngIf="isEmpty()"> - <div adf-empty-list-header class="adf-empty-list-header"> {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.HEADER' | translate}} </div> - <div adf-empty-list-body *ngIf="!isDisabled()"> - <div fxHide.lt-md="true" class="adf-empty-list-drag_drop">{{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.DRAG-AND-DROP.TITLE' | translate}}</div> - <div fxHide.lt-md="true" class="adf-empty-list__any-files-here-to-add"> {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.DRAG-AND-DROP.SUBTITLE' | translate}} </div> - </div> - <div adf-empty-list-footer *ngIf="!isDisabled()"> - <img class="adf-empty-list__empty_doc_lib" [src]="emptyListImageUrl"> - </div> - </adf-empty-list> + <no-content-template> + <ng-template> + <ng-content *ngIf="hasCustomTemplate; else defaulEmptyList" class="adf-custom-empty-template"></ng-content> + <ng-template #defaulEmptyList> + <adf-empty-list> + <div adf-empty-list-header class="adf-empty-list-header"> + {{'ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.HEADER' | translate}} + </div> + </adf-empty-list> + </ng-template> + </ng-template> + </no-content-template> <data-columns> <data-column key="icon" type="image" srTitle="ADF_PROCESS_LIST.PROPERTIES.THUMBNAIL" [sortable]="false"></data-column> diff --git a/lib/process-services/attachment/process-attachment-list.component.spec.ts b/lib/process-services/attachment/process-attachment-list.component.spec.ts index 6575736445..348c3e2c74 100644 --- a/lib/process-services/attachment/process-attachment-list.component.spec.ts +++ b/lib/process-services/attachment/process-attachment-list.component.spec.ts @@ -15,14 +15,14 @@ * limitations under the License. */ -import { NgZone, SimpleChange } from '@angular/core'; +import { NgZone, SimpleChange, Component } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MatProgressSpinnerModule } from '@angular/material'; +import { MaterialModule } from '../material.module'; import { By } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { ProcessContentService } from '@alfresco/adf-core'; import { Observable } from 'rxjs/Observable'; - import { ProcessAttachmentListComponent } from './process-attachment-list.component'; describe('ProcessAttachmentListComponent', () => { @@ -202,22 +202,6 @@ describe('ProcessAttachmentListComponent', () => { }); })); - it('should show the empty list drag and drop component when the process is not completed', async(() => { - getProcessRelatedContentSpy.and.returnValue(Observable.of({ - 'size': 0, - 'total': 0, - 'start': 0, - 'data': [] - })); - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'processInstanceId': change}); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('adf-empty-list .adf-empty-list-drag_drop').innerText.trim()) - .toEqual('ADF_PROCESS_LIST.PROCESS-ATTACHMENT.EMPTY.DRAG-AND-DROP.TITLE'); - }); - })); - it('should not show the empty list drag and drop component when is disabled', async(() => { getProcessRelatedContentSpy.and.returnValue(Observable.of({ 'size': 0, @@ -308,3 +292,45 @@ describe('ProcessAttachmentListComponent', () => { }); }); + +@Component({ + template: ` + <adf-process-attachment-list> + <adf-empty-list> + <div adf-empty-list-header class="adf-empty-list-header">Custom header</div> + </adf-empty-list> + </adf-process-attachment-list> + ` +}) +class CustomEmptyTemplateComponent { +} + +describe('Custom CustomEmptyTemplateComponent', () => { + let fixture: ComponentFixture<CustomEmptyTemplateComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ProcessAttachmentListComponent, + CustomEmptyTemplateComponent + ], + imports: [ + MaterialModule + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CustomEmptyTemplateComponent); + fixture.detectChanges(); + }); + + it('should render the custom template', () => { + fixture.whenStable().then(() => { + fixture.detectChanges(); + let title: any = fixture.debugElement.queryAll(By.css('[adf-empty-list-header]')); + expect(title.length).toBe(1); + expect(title[0].nativeElement.innerText).toBe('Custom header'); + }); + }); +}); diff --git a/lib/process-services/attachment/process-attachment-list.component.ts b/lib/process-services/attachment/process-attachment-list.component.ts index da1c40e4a4..50976948b9 100644 --- a/lib/process-services/attachment/process-attachment-list.component.ts +++ b/lib/process-services/attachment/process-attachment-list.component.ts @@ -15,16 +15,20 @@ * limitations under the License. */ -import { ContentService, ThumbnailService } from '@alfresco/adf-core'; -import { Component, EventEmitter, Input, NgZone, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { ContentService, EmptyListComponent , ThumbnailService } from '@alfresco/adf-core'; +import { AfterContentInit, ContentChild, Component, EventEmitter, Input, NgZone, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ProcessContentService } from '@alfresco/adf-core'; @Component({ selector: 'adf-process-attachment-list', styleUrls: ['./process-attachment-list.component.scss'], - templateUrl: './process-attachment-list.component.html' + templateUrl: './process-attachment-list.component.html', + encapsulation: ViewEncapsulation.None }) -export class ProcessAttachmentListComponent implements OnChanges { +export class ProcessAttachmentListComponent implements OnChanges, AfterContentInit { + + @ContentChild(EmptyListComponent) + emptyTemplate: EmptyListComponent; @Input() processInstanceId: string; @@ -41,8 +45,7 @@ export class ProcessAttachmentListComponent implements OnChanges { @Output() error: EventEmitter<any> = new EventEmitter<any>(); - @Input() - emptyListImageUrl: string = './assets/images/empty_doc_lib.svg'; + hasCustomTemplate: boolean = false; attachments: any[] = []; isLoading: boolean = true; @@ -59,6 +62,12 @@ export class ProcessAttachmentListComponent implements OnChanges { } } + ngAfterContentInit() { + if (this.emptyTemplate) { + this.hasCustomTemplate = true; + } + } + reset() { this.attachments = []; } @@ -69,6 +78,10 @@ export class ProcessAttachmentListComponent implements OnChanges { }); } + hasCutomEmptyTemplate() { + return !!this.emptyTemplate; + } + add(content: any): void { this.ngZone.run(() => { this.attachments.push({ diff --git a/lib/process-services/attachment/task-attachment-list.component.spec.ts b/lib/process-services/attachment/task-attachment-list.component.spec.ts index 9c957d22c5..16fb136d84 100644 --- a/lib/process-services/attachment/task-attachment-list.component.spec.ts +++ b/lib/process-services/attachment/task-attachment-list.component.spec.ts @@ -331,9 +331,11 @@ describe('Custom CustomEmptyTemplateComponent', () => { }); it('should render the custom template', () => { - fixture.detectChanges(); - let title: any = fixture.debugElement.queryAll(By.css('[adf-empty-list-header]')); - expect(title.length).toBe(1); - expect(title[0].nativeElement.innerText).toBe('Custom header'); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let title: any = fixture.debugElement.queryAll(By.css('[adf-empty-list-header]')); + expect(title.length).toBe(1); + expect(title[0].nativeElement.innerText).toBe('Custom header'); + }); }); }); diff --git a/lib/process-services/attachment/task-attachment-list.component.ts b/lib/process-services/attachment/task-attachment-list.component.ts index f30ab4d23f..e2ff5606bb 100644 --- a/lib/process-services/attachment/task-attachment-list.component.ts +++ b/lib/process-services/attachment/task-attachment-list.component.ts @@ -16,7 +16,7 @@ */ import { ContentService, ThumbnailService, EmptyListComponent } from '@alfresco/adf-core'; -import { AfterContentInit, ContentChild, Component, ElementRef, EventEmitter, Input, NgZone, OnChanges, Output, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { AfterContentInit, ContentChild, Component, EventEmitter, Input, NgZone, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; import { ProcessContentService } from '@alfresco/adf-core'; @Component({ @@ -47,11 +47,6 @@ export class TaskAttachmentListComponent implements OnChanges, AfterContentInit hasCustomTemplate: boolean = false; - @ViewChild('customEmptyListTemplate') - customTemplateRef: ElementRef; - - curret: any; - attachments: any[] = []; isLoading: boolean = false; diff --git a/lib/process-services/comments/comment-list.component.ts b/lib/process-services/comments/comment-list.component.ts index 3f4bc1f27b..92d357f838 100644 --- a/lib/process-services/comments/comment-list.component.ts +++ b/lib/process-services/comments/comment-list.component.ts @@ -27,9 +27,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; export class CommentListComponent { + /** The comments data used to populate the list. */ @Input() comments: CommentProcessModel[]; + /** Emitted when the user clicks on one of the comment rows. */ @Output() clickRow: EventEmitter<CommentProcessModel> = new EventEmitter<CommentProcessModel>(); diff --git a/lib/process-services/comments/comments.component.ts b/lib/process-services/comments/comments.component.ts index 08102ae930..eece99d06b 100644 --- a/lib/process-services/comments/comments.component.ts +++ b/lib/process-services/comments/comments.component.ts @@ -27,12 +27,15 @@ import { Observer } from 'rxjs/Observer'; }) export class CommentsComponent implements OnChanges { + /** The numeric ID of the task. */ @Input() taskId: string; + /** Are the comments read only? */ @Input() readOnly: boolean = false; + /** Emitted when an error occurs while displaying/adding a comment. */ @Output() error: EventEmitter<any> = new EventEmitter<any>(); diff --git a/lib/process-services/content-widget/attach-file-widget.component.html b/lib/process-services/content-widget/attach-file-widget.component.html new file mode 100644 index 0000000000..561efd3ac3 --- /dev/null +++ b/lib/process-services/content-widget/attach-file-widget.component.html @@ -0,0 +1,83 @@ +<div class="adf-attach-widget {{field.className}}" + [class.adf-invalid]="!field.isValid" + [class.adf-readonly]="field.readOnly"> + <label class="adf-label" [attr.for]="field.id">{{field.name}} + <span *ngIf="isRequired()">*</span> + </label> + <div class="adf-attach-widget-container"> + <div id="adf-attach-widget-simple-upload" *ngIf="isSimpleUploadButton() && isUploadButtonVisible()"> + <a mat-raised-button color="primary"> + {{ 'FORM.FIELD.UPLOAD' | translate }} + <mat-icon>file_upload</mat-icon> + <input #uploadFiles + [multiple]="multipleOption" + type="file" + [id]="field.id" + (change)="onFileChanged($event)" /> + </a> + </div> + <div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible() && isMultipleSourceUpload()"> + <button mat-raised-button color="primary" [matMenuTriggerFor]="menu" [id]="field.id"> + {{ 'FORM.FIELD.UPLOAD' | translate }} + <mat-icon>attach_file</mat-icon> + </button> + <mat-menu #menu="matMenu" class="adf-attach-widget__menu-content"> + <button mat-menu-item (click)="uploadFile.click()" + id="attach-local-file" + *ngIf="isAllFileSourceSelected()"> + {{ 'FORM.FIELD.LOCALSTORAGE' | translate }} + <mat-icon>file_upload</mat-icon> + <input #uploadFile + class="adf-attach-widget__input-type" + [multiple]="multipleOption" + type="file" + [id]="field.id" + (change)="onFileChanged($event)" /> + </button> + <button mat-menu-item + *ngIf="isDefinedSourceFolder()" + id="attach-{{field.params?.fileSource?.name}}" + (click)="openSelectDialogFromFileSource()"> + {{field.params?.fileSource?.name}} + <mat-icon> + <img class="adf-attach-widget__image-logo" src="../assets/images/alfresco-flower.svg"> + </mat-icon> + </button> + <div *ngIf="!isDefinedSourceFolder()"> + <button mat-menu-item *ngFor="let repo of repositoryList" + id="attach-{{repo?.name}}" + (click)="openSelectDialog(repo.id, repo.name)"> + {{repo.name}} + <mat-icon> + <img class="adf-attach-widget__image-logo" src="../assets/images/alfresco-flower.svg"> + </mat-icon> + </button> + </div> + </mat-menu> + </div> + </div> +</div> + +<div id="adf-attach-widget-readonly-list"> + <mat-list *ngIf="hasFile"> + <mat-list-item class="adf-attach-files-row" *ngFor="let file of field.value"> + <img mat-list-icon class="adf-attach-widget__icon" + [id]="'file-'+file.id+'-icon'" + [src]="getIcon(file.mimeType)" + [alt]="mimeTypeIcon" + (click)="fileClicked(file)" + (keyup.enter)="fileClicked(file)" + role="button" + tabindex="0"/> + <span matLine id="{{'file-'+file.id}}" (click)="fileClicked(file)" (keyup.enter)="fileClicked(file)" + role="button" tabindex="0" class="adf-file">{{file.name}}</span> + <button *ngIf="!field.readOnly" mat-icon-button [id]="'file-'+file.id+'-remove'" + (click)="removeFile(file);" (keyup.enter)="removeFile(file);"> + <mat-icon class="mat-24">highlight_off</mat-icon> + </button> + </mat-list-item> + </mat-list> +</div> + +<error-widget [error]="field.validationSummary"></error-widget> +<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget> diff --git a/lib/process-services/content-widget/attach-file-widget.component.scss b/lib/process-services/content-widget/attach-file-widget.component.scss new file mode 100644 index 0000000000..61b904af33 --- /dev/null +++ b/lib/process-services/content-widget/attach-file-widget.component.scss @@ -0,0 +1,73 @@ +.adf { + + &-attach-widget-container { + margin-bottom: 15px; + display: flex; + align-items: center; + + input { + cursor: pointer; + height: 100%; + right: 0; + opacity: 0; + position: absolute; + top: 0; + width: 300px; + z-index: 4; + } + } + + &-attach-widget__menu-upload { + display: flex; + align-items: center; + } + + &-attach-widget__input-type { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; + } + + &-attach-widget__image-logo{ + padding-left: 5px; + } + + &-attach-widget-repo-button{ + padding-left: 10px; + + .mat-button-wrapper { + display: inline; + } + + .mat-mini-fab.mat-accent { + background-color: inherit; + } + } + + &-attach-widget { + width: 100%; + word-break: break-all; + padding: 0.4375em 0; + border-top: 0.84375em solid transparent; + } + + &-attach-widget__icon { + padding: 6px; + float: left; + cursor: pointer; + } + + &-attach-widget__reset { + margin-top: -2px; + } + + &-attach-files-row { + .mat-line { + margin-bottom: 0px; + } + } + +} diff --git a/lib/process-services/content-widget/attach-file-widget.component.ts b/lib/process-services/content-widget/attach-file-widget.component.ts new file mode 100644 index 0000000000..9eab0f881f --- /dev/null +++ b/lib/process-services/content-widget/attach-file-widget.component.ts @@ -0,0 +1,142 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/* tslint:disable:component-selector */ + +import { Component, ViewEncapsulation, OnInit } from '@angular/core'; +import { + baseHost, + UploadWidgetComponent, + FormService, + LogService, + ThumbnailService, + ProcessContentService, + ActivitiContentService +} from '@alfresco/adf-core'; +import { ContentNodeDialogService } from '@alfresco/adf-content-services'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + selector: 'attach-widget', + templateUrl: './attach-file-widget.component.html', + styleUrls: ['./attach-file-widget.component.scss'], + host: baseHost, + encapsulation: ViewEncapsulation.None +}) +export class AttachFileWidgetComponent extends UploadWidgetComponent implements OnInit { + + repositoryList = []; + + constructor(public formService: FormService, + private logger: LogService, + public thumbnails: ThumbnailService, + public processContentService: ProcessContentService, + private activitiContentService: ActivitiContentService, + private contentDialog: ContentNodeDialogService) { + super(formService, logger, thumbnails, processContentService); + } + + ngOnInit() { + if (this.field && + this.field.value && + this.field.value.length > 0) { + this.hasFile = true; + } + this.getMultipleFileParam(); + this.activitiContentService.getAlfrescoRepositories(null, true).subscribe((repoList) => { + this.repositoryList = repoList; + }); + } + + isFileSourceConfigured(): boolean { + return !!this.field.params && !!this.field.params.fileSource; + } + + isMultipleSourceUpload(): boolean { + return !this.field.readOnly && this.isFileSourceConfigured() && !this.isOnlyLocalSourceSelected(); + } + + isAllFileSourceSelected(): boolean { + return this.field.params && + this.field.params.fileSource && + this.field.params.fileSource.serviceId === 'all-file-sources'; + } + + isOnlyLocalSourceSelected(): boolean { + return this.field.params && + this.field.params.fileSource && + this.field.params.fileSource.serviceId === 'local-file'; + } + + isSimpleUploadButton(): boolean { + return this.isUploadButtonVisible() && + !this.isFileSourceConfigured() || + this.isOnlyLocalSourceSelected(); + } + + isUploadButtonVisible(): boolean { + return (!this.hasFile || this.multipleOption) && !this.field.readOnly; + } + + isDefinedSourceFolder(): boolean { + return !!this.field.params && + !!this.field.params.fileSource && + !!this.field.params.fileSource.selectedFolder; + } + + openSelectDialogFromFileSource() { + let params = this.field.params; + if (this.isDefinedSourceFolder()) { + this.contentDialog.openFileBrowseDialogByFolderId(params.fileSource.selectedFolder.pathId).subscribe( + (selections: MinimalNodeEntryEntity[]) => { + this.uploadFileFromCS(selections, + this.field.params.fileSource.selectedFolder.accountId, + this.field.params.fileSource.selectedFolder.siteId); + }); + } + } + + openSelectDialog(repoId: string, repoName: string) { + const accountIdentifier = 'alfresco-' + repoId + repoName; + this.contentDialog.openFileBrowseDialogBySite().subscribe( + (selections: MinimalNodeEntryEntity[]) => { + this.uploadFileFromCS(selections, accountIdentifier); + }); + } + + private uploadFileFromCS(fileNodeList: MinimalNodeEntryEntity[], accountId: string, siteId?: string) { + let filesSaved = []; + Observable.from(fileNodeList) + .mergeMap(node => + this.activitiContentService.applyAlfrescoNode(node, + siteId, + accountId) + ).subscribe((res) => { + filesSaved.push(res); + }, + (error) => { + this.logger.error(error); + }, + () => { + this.field.value = filesSaved; + this.field.json.value = filesSaved; + }); + this.hasFile = true; + } + +} diff --git a/lib/process-services/content-widget/attach-file-widget.components.spec.ts b/lib/process-services/content-widget/attach-file-widget.components.spec.ts new file mode 100644 index 0000000000..5f52021e8b --- /dev/null +++ b/lib/process-services/content-widget/attach-file-widget.components.spec.ts @@ -0,0 +1,243 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { AttachFileWidgetComponent } from './attach-file-widget.component'; +import { + FormFieldModel, + FormModel, + FormFieldTypes, + FormService, + ProcessContentService, + ActivitiContentService, + AlfrescoApiService, + LogService, + ThumbnailService, + SitesService, + FormFieldMetadata +} from '@alfresco/adf-core'; +import { ContentNodeDialogService, DocumentListService } from '@alfresco/adf-content-services'; +import { MaterialModule } from '../material.module'; +import { Observable } from 'rxjs/Observable'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; + +const fakeRepositoryListAnswer = [ + { + 'authorized': true, + 'serviceId': 'alfresco-9999-SHAREME', + 'metaDataAllowed': true, + 'name': 'SHAREME' + }, + { + 'authorized': true, + 'serviceId': 'alfresco-0000-GOKUSHARE', + 'metaDataAllowed': true, + 'name': 'GOKUSHARE' + }]; + +const onlyLocalParams = { + fileSource : { + serviceId: 'local-file' + } +}; + +const allSourceParams = { + fileSource : { + serviceId: 'all-file-sources' + } +}; + +const definedSourceParams = { + fileSource : { + serviceId: 'goku-sources', + name: 'pippo-baudo', + selectedFolder: { + accountId: 'goku-share-account-id' + } + } +}; + +const fakeMinimalNode: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> { + id: 'fake', + name: 'fake-name' +}; + +const fakePngAnswer = { + 'id': 1155, + 'name': 'a_png_file.png', + 'created': '2017-07-25T17:17:37.099Z', + 'createdBy': {'id': 1001, 'firstName': 'Admin', 'lastName': 'admin', 'email': 'admin'}, + 'relatedContent': false, + 'contentAvailable': true, + 'link': false, + 'mimeType': 'image/png', + 'simpleType': 'image', + 'previewStatus': 'queued', + 'thumbnailStatus': 'queued' +}; + +describe('AttachFileWidgetComponent', () => { + + let widget: AttachFileWidgetComponent; + let fixture: ComponentFixture<AttachFileWidgetComponent>; + let element: HTMLInputElement; + let activitiContentService: ActivitiContentService; + let contentNodeDialogService: ContentNodeDialogService; + let processContentService: ProcessContentService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MaterialModule], + declarations: [AttachFileWidgetComponent], + providers: [ + FormService, + ProcessContentService, + ThumbnailService, + ActivitiContentService, + AlfrescoApiService, + LogService, + SitesService, + DocumentListService, + ContentNodeDialogService + ] + }).compileComponents().then(() => { + fixture = TestBed.createComponent(AttachFileWidgetComponent); + widget = fixture.componentInstance; + element = fixture.nativeElement; + activitiContentService = TestBed.get(ActivitiContentService); + contentNodeDialogService = TestBed.get(ContentNodeDialogService); + processContentService = TestBed.get(ProcessContentService); + }); + })); + + afterEach(() => { + fixture.destroy(); + }); + + it('should be able to create the widget', () => { + expect(widget).not.toBeNull(); + }); + + it('should show up as simple upload when is configured for only local files', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + widget.field.id = 'simple-upload-button'; + widget.field.params = <FormFieldMetadata> onlyLocalParams; + fixture.detectChanges(); + expect(element.querySelector('#simple-upload-button')).not.toBeNull(); + }); + + it('should show up all the repository option on menu list', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + widget.field.id = 'attach-file-attach'; + widget.field.params = <FormFieldMetadata> allSourceParams; + spyOn(activitiContentService, 'getAlfrescoRepositories').and.returnValue(Observable.of(fakeRepositoryListAnswer)); + fixture.detectChanges(); + + let attachButton: HTMLButtonElement = element.querySelector('#attach-file-attach'); + expect(attachButton).not.toBeNull(); + attachButton.click(); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(By.css('#attach-local-file'))).not.toBeNull(); + expect(fixture.debugElement.queryAll(By.css('#attach-local-file'))).not.toBeUndefined(); + expect(fixture.debugElement.queryAll(By.css('#attach-SHAREME'))).not.toBeNull(); + expect(fixture.debugElement.queryAll(By.css('#attach-SHAREME'))).not.toBeUndefined(); + expect(fixture.debugElement.queryAll(By.css('#attach-GOKUSHARE'))).not.toBeNull(); + expect(fixture.debugElement.queryAll(By.css('#attach-GOKUSHARE'))).not.toBeUndefined(); + }); + + it('should be able to upload files coming from content node selector', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + widget.field.id = 'attach-file-attach'; + widget.field.params = <FormFieldMetadata> allSourceParams; + spyOn(activitiContentService, 'getAlfrescoRepositories').and.returnValue(Observable.of(fakeRepositoryListAnswer)); + spyOn(activitiContentService, 'applyAlfrescoNode').and.returnValue(Observable.of(fakePngAnswer)); + spyOn(contentNodeDialogService, 'openFileBrowseDialogBySite').and.returnValue(Observable.of([fakeMinimalNode])); + fixture.detectChanges(); + + let attachButton: HTMLButtonElement = element.querySelector('#attach-file-attach'); + expect(attachButton).not.toBeNull(); + attachButton.click(); + fixture.detectChanges(); + fixture.debugElement.query(By.css('#attach-SHAREME')).nativeElement.click(); + fixture.detectChanges(); + + expect(element.querySelector('#file-1155-icon')).not.toBeNull(); + }); + + it('should be able to upload files when a defined folder is selected', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + widget.field.id = 'attach-file-attach'; + widget.field.params = <FormFieldMetadata> definedSourceParams; + spyOn(activitiContentService, 'getAlfrescoRepositories').and.returnValue(Observable.of(fakeRepositoryListAnswer)); + spyOn(activitiContentService, 'applyAlfrescoNode').and.returnValue(Observable.of(fakePngAnswer)); + spyOn(contentNodeDialogService, 'openFileBrowseDialogByFolderId').and.returnValue(Observable.of([fakeMinimalNode])); + fixture.detectChanges(); + + let attachButton: HTMLButtonElement = element.querySelector('#attach-file-attach'); + expect(attachButton).not.toBeNull(); + attachButton.click(); + fixture.detectChanges(); + fixture.debugElement.query(By.css('#attach-pippo-baudo')).nativeElement.click(); + fixture.detectChanges(); + + expect(element.querySelector('#file-1155-icon')).not.toBeNull(); + }); + + it('should be able to upload files from local source', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [] + }); + widget.field.id = 'attach-file-attach'; + widget.field.params = <FormFieldMetadata> onlyLocalParams; + spyOn(processContentService, 'createTemporaryRawRelatedContent').and.returnValue(Observable.of(fakePngAnswer)); + fixture.detectChanges(); + + let inputDebugElement = fixture.debugElement.query(By.css('#attach-file-attach')); + inputDebugElement.triggerEventHandler('change', {target: {files: [fakePngAnswer]}}); + fixture.detectChanges(); + + expect(element.querySelector('#file-1155-icon')).not.toBeNull(); + }); + + it('should display file list when field has value', () => { + widget.field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.UPLOAD, + value: [fakePngAnswer] + }); + widget.field.id = 'attach-file-attach'; + widget.field.params = <FormFieldMetadata> onlyLocalParams; + fixture.detectChanges(); + + expect(element.querySelector('#file-1155-icon')).not.toBeNull(); + }); + +}); diff --git a/lib/process-services/content-widget/attach-folder-widget.component.html b/lib/process-services/content-widget/attach-folder-widget.component.html new file mode 100644 index 0000000000..9d67f76422 --- /dev/null +++ b/lib/process-services/content-widget/attach-folder-widget.component.html @@ -0,0 +1,30 @@ +<div class="adf-attach-folder-widget {{field.className}}" + [class.adf-invalid]="!field.isValid" + [class.adf-readonly]="field.readOnly"> + <label class="adf-label" [attr.for]="field.id">{{field.name}}<span *ngIf="isRequired()">*</span></label> + <div class="adf-attach-folder-widget-container"> + <div *ngIf="hasFolder" class="adf-attach-folder-result"> + <mat-icon>folder</mat-icon> + <div class="adf-attach-folder-files-row"> + <span matLine id="{{'folder-'+field?.id}}" + role="button" tabindex="0" class="adf-folder">{{selectedFolderName}}</span> + <button *ngIf="!field.readOnly" mat-icon-button [id]="'folder-'+field?.id+'-remove'" + (click)="removeFolder();"> + <mat-icon class="mat-24">highlight_off</mat-icon> + </button> + </div> + </div> + + <div class="button-row" *ngIf="!hasFolder && !field.readOnly"> + <button mat-raised-button + color="primary" + (click)="openSelectDialogFromFileSource()" + [id]="'folder-'+field?.id+'-button'"> + {{ 'FORM.FIELD.UPLOAD' | translate }} + <mat-icon>cloud_upload</mat-icon> + </button> + </div> + </div> + <error-widget [error]="field.validationSummary"></error-widget> + <error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget> +</div> diff --git a/lib/process-services/content-widget/attach-folder-widget.component.scss b/lib/process-services/content-widget/attach-folder-widget.component.scss new file mode 100644 index 0000000000..a7226dbf0c --- /dev/null +++ b/lib/process-services/content-widget/attach-folder-widget.component.scss @@ -0,0 +1,39 @@ +.adf { + + &-attach-folder-widget-container { + margin-bottom: 15px; + display: flex; + align-items: center; + + input { + cursor: pointer; + height: 100%; + right: 0; + opacity: 0; + position: absolute; + top: 0; + width: 300px; + z-index: 4; + } + } + + &-attach-folder-widget { + width: 100%; + word-break: break-all; + padding: 0.4375em 0; + border-top: 0.84375em solid transparent; + } + + &-attach-folder-files-row { + padding-left: 8px; + .mat-line { + margin-bottom: 0px; + } + } + + &-attach-folder-result { + display: flex; + align-items: center; + } + +} diff --git a/lib/process-services/content-widget/attach-folder-widget.component.spec.ts b/lib/process-services/content-widget/attach-folder-widget.component.spec.ts new file mode 100644 index 0000000000..3f72aed067 --- /dev/null +++ b/lib/process-services/content-widget/attach-folder-widget.component.spec.ts @@ -0,0 +1,169 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { AttachFolderWidgetComponent } from './attach-folder-widget.component'; +import { + FormFieldModel, + FormModel, + FormService, + AlfrescoApiService, + LogService, + ThumbnailService, + SitesService, + NodesApiService +} from '@alfresco/adf-core'; +import { ContentNodeDialogService, DocumentListService } from '@alfresco/adf-content-services'; +import { MaterialModule } from '../material.module'; +import { Observable } from 'rxjs/Observable'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; + +const fakeMinimalNode: MinimalNodeEntryEntity = <MinimalNodeEntryEntity> { + id: 'fake', + name: 'fake-name' +}; + +const definedSourceParams = { + folderSource : { + serviceId: 'goofy-sources', + name: 'pippo-baudo', + selectedFolder: { + accountId: 'goku-share-account-id', + pathId: 'fake-pippo-baudo-id' + } + } +}; + +describe('AttachFolderWidgetComponent', () => { + + let widget: AttachFolderWidgetComponent; + let fixture: ComponentFixture<AttachFolderWidgetComponent>; + let element: HTMLInputElement; + let contentNodeDialogService: ContentNodeDialogService; + let nodeService: NodesApiService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MaterialModule], + declarations: [AttachFolderWidgetComponent], + providers: [ + FormService, + ThumbnailService, + AlfrescoApiService, + LogService, + SitesService, + DocumentListService, + ContentNodeDialogService, + NodesApiService + ] + }).compileComponents().then(() => { + fixture = TestBed.createComponent(AttachFolderWidgetComponent); + widget = fixture.componentInstance; + element = fixture.nativeElement; + contentNodeDialogService = TestBed.get(ContentNodeDialogService); + nodeService = TestBed.get(NodesApiService); + }); + })); + + afterEach(() => { + fixture.destroy(); + }); + + it('should be able to create the widget', () => { + expect(widget).not.toBeNull(); + }); + + it('should be rendered correctly', () => { + expect(widget).not.toBeNull(); + widget.field = new FormFieldModel(new FormModel(), { + type: 'select-folder', + id: 'fake-widget', + value: null + }); + fixture.detectChanges(); + expect(element.querySelector('#folder-fake-widget-button')).toBeDefined(); + expect(element.querySelector('#folder-fake-widget-button')).not.toBeNull(); + }); + + it('should show the folder selected by content node', async(() => { + spyOn(contentNodeDialogService, 'openFolderBrowseDialogBySite').and.returnValue(Observable.of([fakeMinimalNode])); + expect(widget).not.toBeNull(); + widget.field = new FormFieldModel(new FormModel(), { + type: 'select-folder', + id: 'fake-widget', + value: null + }); + fixture.detectChanges(); + fixture.debugElement.query(By.css('#folder-fake-widget-button')).nativeElement.click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.querySelector('#folder-fake-widget')).not.toBeNull(); + }); + })); + + it('should show the folder selected by content node opening on a configured folder', async(() => { + spyOn(contentNodeDialogService, 'openFolderBrowseDialogByFolderId').and.returnValue(Observable.of([fakeMinimalNode])); + expect(widget).not.toBeNull(); + widget.field = new FormFieldModel(new FormModel(), { + type: 'select-folder', + id: 'fake-widget', + value: null, + params: definedSourceParams + }); + fixture.detectChanges(); + fixture.debugElement.query(By.css('#folder-fake-widget-button')).nativeElement.click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.querySelector('#folder-fake-widget')).not.toBeNull(); + }); + })); + + it('should retrieve the node information on init', async(() => { + spyOn(nodeService, 'getNode').and.returnValue(Observable.of(fakeMinimalNode)); + expect(widget).not.toBeNull(); + widget.field = new FormFieldModel(new FormModel(), { + type: 'select-folder', + id: 'fake-widget', + value: 'fake-pippo-baudo-id' + }); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.querySelector('#folder-fake-widget')).not.toBeNull(); + expect(element.querySelector('#folder-fake-widget-button')).toBeNull(); + }); + })); + + it('should remove the folder via the remove button', async(() => { + spyOn(nodeService, 'getNode').and.returnValue(Observable.of(fakeMinimalNode)); + expect(widget).not.toBeNull(); + widget.field = new FormFieldModel(new FormModel(), { + type: 'select-folder', + id: 'fake-widget', + value: 'fake-pippo-baudo-id' + }); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(element.querySelector('#folder-fake-widget')).not.toBeNull(); + expect(element.querySelector('#folder-fake-widget-button')).toBeNull(); + fixture.debugElement.query(By.css('#folder-fake-widget-remove')).nativeElement.click(); + fixture.detectChanges(); + expect(element.querySelector('#folder-fake-widget')).toBeNull(); + }); + })); + +}); diff --git a/lib/process-services/content-widget/attach-folder-widget.component.ts b/lib/process-services/content-widget/attach-folder-widget.component.ts new file mode 100644 index 0000000000..c8769f2f97 --- /dev/null +++ b/lib/process-services/content-widget/attach-folder-widget.component.ts @@ -0,0 +1,89 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +/* tslint:disable:component-selector*/ + +import { Component, ViewEncapsulation, OnInit } from '@angular/core'; +import { + baseHost, + WidgetComponent, + FormService, + NodesApiService +} from '@alfresco/adf-core'; +import { ContentNodeDialogService } from '@alfresco/adf-content-services'; +import { MinimalNodeEntryEntity } from 'alfresco-js-api'; + +@Component({ + selector: 'attach-folder-widget', + templateUrl: './attach-folder-widget.component.html', + styleUrls: ['./attach-folder-widget.component.scss'], + host: baseHost, + encapsulation: ViewEncapsulation.None +}) +export class AttachFolderWidgetComponent extends WidgetComponent implements OnInit { + + hasFolder: boolean = false; + selectedFolderName: string = ''; + + constructor(private contentDialog: ContentNodeDialogService, + public formService: FormService, + private nodeService: NodesApiService) { + super(); + } + + ngOnInit() { + if (this.field && + this.field.value) { + this.hasFolder = true; + this.nodeService.getNode(this.field.value).subscribe((node: MinimalNodeEntryEntity) => { + this.selectedFolderName = node.name; + }); + } + } + + isDefinedSourceFolder(): boolean { + return !!this.field.params && + !!this.field.params.folderSource && + !!this.field.params.folderSource.selectedFolder; + } + + openSelectDialogFromFileSource() { + let params = this.field.params; + if (this.isDefinedSourceFolder()) { + this.contentDialog.openFolderBrowseDialogByFolderId(params.folderSource.selectedFolder.pathId).subscribe( + (selections: MinimalNodeEntryEntity[]) => { + this.selectedFolderName = selections[0].name; + this.field.value = selections[0].id; + this.hasFolder = true; + }); + } else { + this.contentDialog.openFolderBrowseDialogBySite().subscribe( + (selections: MinimalNodeEntryEntity[]) => { + this.selectedFolderName = selections[0].name; + this.field.value = selections[0].id; + this.hasFolder = true; + }); + } + } + + removeFolder() { + this.field.value = null; + this.selectedFolderName = ''; + this.hasFolder = false; + } + +} diff --git a/lib/process-services/content-widget/content-widget.module.ts b/lib/process-services/content-widget/content-widget.module.ts new file mode 100644 index 0000000000..336ca2705d --- /dev/null +++ b/lib/process-services/content-widget/content-widget.module.ts @@ -0,0 +1,52 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MaterialModule } from '../material.module'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { DataColumnModule, DataTableModule, FormModule } from '@alfresco/adf-core'; + +import { AttachFileWidgetComponent } from './attach-file-widget.component'; +import { AttachFolderWidgetComponent } from './attach-folder-widget.component'; + +@NgModule({ + imports: [ + DataColumnModule, + DataTableModule, + FormsModule, + ReactiveFormsModule, + MaterialModule, + CommonModule, + TranslateModule, + FormModule + ], + entryComponents: [ + AttachFileWidgetComponent, + AttachFolderWidgetComponent + ], + declarations: [ + AttachFileWidgetComponent, + AttachFolderWidgetComponent + ], + exports: [ + AttachFileWidgetComponent, + AttachFolderWidgetComponent + ] +}) +export class ContentWidgetModule {} diff --git a/lib/process-services/content-widget/index.ts b/lib/process-services/content-widget/index.ts new file mode 100644 index 0000000000..4c6ac1d58f --- /dev/null +++ b/lib/process-services/content-widget/index.ts @@ -0,0 +1,18 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './public-api'; diff --git a/lib/process-services/content-widget/public-api.ts b/lib/process-services/content-widget/public-api.ts new file mode 100644 index 0000000000..592b6804b9 --- /dev/null +++ b/lib/process-services/content-widget/public-api.ts @@ -0,0 +1,19 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +export * from './attach-file-widget.component'; +export * from './attach-folder-widget.component'; diff --git a/lib/process-services/i18n/de.json b/lib/process-services/i18n/de.json index ecac7c02db..03f0cc0f47 100644 --- a/lib/process-services/i18n/de.json +++ b/lib/process-services/i18n/de.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Prozess", "NAME": "Name", + "THUMBNAIL": "Miniaturansicht", "STATUS": "Status", - "END_DATE": "Fälligkeitsdatum", + "END_DATE": "Enddatum", "END_DATE_DEFAULT": "Kein Datum", "CATEGORY": "Kategorie", "CATEGORY_DEFAULT": "Keine Kategorie", @@ -294,5 +295,20 @@ "CREATED-ON": "Erstellt am" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Erstellen", + "NEW_TASK": "Neue Aufgabe", + "NEW_PROCESS": "Neuer Prozess" + } + }, + "APP": { + "DIALOG": { + "START": "Start", + "TITLE": "Prozess auswählen", + "LIST": "Prozessliste", + "ERROR": "Bei der Verbindung zu Process Services ist ein Fehler aufgetreten" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/en.json b/lib/process-services/i18n/en.json index 746b13af4b..94ca834091 100644 --- a/lib/process-services/i18n/en.json +++ b/lib/process-services/i18n/en.json @@ -295,5 +295,20 @@ "CREATED-ON": "Created on" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "CREATE", + "NEW_TASK": "New Task", + "NEW_PROCESS": "New Process" + } + }, + "APP":{ + "DIALOG" :{ + "START" : "Start", + "TITLE" : "Select a process", + "LIST": "Process list", + "ERROR" : "There is a problem connecting to Process Services" + } } } diff --git a/lib/process-services/i18n/es.json b/lib/process-services/i18n/es.json index f4749aee9b..36020188ef 100644 --- a/lib/process-services/i18n/es.json +++ b/lib/process-services/i18n/es.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Proceso", "NAME": "Nombre", + "THUMBNAIL": "Miniatura", "STATUS": "Estado", - "END_DATE": "Fecha de vencimiento", + "END_DATE": "Fecha de fin", "END_DATE_DEFAULT": "Sin fecha", "CATEGORY": "Categoría", "CATEGORY_DEFAULT": "Sin categoría", @@ -294,5 +295,20 @@ "CREATED-ON": "Creado" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Crear", + "NEW_TASK": "Nueva tarea", + "NEW_PROCESS": "Nuevo proceso" + } + }, + "APP": { + "DIALOG": { + "START": "Comenzar", + "TITLE": "Seleccionar proceso", + "LIST": "Lista de procesos", + "ERROR": "Hay un problema con la conexión a Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/fr.json b/lib/process-services/i18n/fr.json index 82ef672db6..09e4c6524b 100644 --- a/lib/process-services/i18n/fr.json +++ b/lib/process-services/i18n/fr.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Processus", "NAME": "Nom", + "THUMBNAIL": "Miniature", "STATUS": "Statut", - "END_DATE": "Date d'échéance", + "END_DATE": "Date de fin", "END_DATE_DEFAULT": "Aucune date", "CATEGORY": "Catégorie", "CATEGORY_DEFAULT": "Aucune catégorie", @@ -294,5 +295,20 @@ "CREATED-ON": "Créé le" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Créer", + "NEW_TASK": "Nouvelle tâche", + "NEW_PROCESS": "Nouveau processus" + } + }, + "APP": { + "DIALOG": { + "START": "Démarrer", + "TITLE": "Sélectionner un processus", + "LIST": "Liste de processus", + "ERROR": "Nous rencontrons un problème de connexion à Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/it.json b/lib/process-services/i18n/it.json index 524e7ef081..8d0573512f 100644 --- a/lib/process-services/i18n/it.json +++ b/lib/process-services/i18n/it.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Processo", "NAME": "Nome", + "THUMBNAIL": "Miniatura", "STATUS": "Stato", - "END_DATE": "Scadenza", + "END_DATE": "Data di fine", "END_DATE_DEFAULT": "Nessuna data", "CATEGORY": "Categoria", "CATEGORY_DEFAULT": "Nessuna categoria", @@ -294,5 +295,20 @@ "CREATED-ON": "Creato il" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Crea", + "NEW_TASK": "Nuovo compito", + "NEW_PROCESS": "Nuovo processo" + } + }, + "APP": { + "DIALOG": { + "START": "Avvia", + "TITLE": "Seleziona processo", + "LIST": "Elenco processi", + "ERROR": "Si è verificato un problema di connessione a Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/ja.json b/lib/process-services/i18n/ja.json index 76dcc7bee0..4b2d530afe 100644 --- a/lib/process-services/i18n/ja.json +++ b/lib/process-services/i18n/ja.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "プロセス", "NAME": "名前", + "THUMBNAIL": "サムネイル", "STATUS": "ステータス", - "END_DATE": "期限", + "END_DATE": "終了日", "END_DATE_DEFAULT": "期限なし", "CATEGORY": "カテゴリ", "CATEGORY_DEFAULT": "カテゴリなし", @@ -294,5 +295,20 @@ "CREATED-ON": "作成日" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "作成", + "NEW_TASK": "新しいタスク", + "NEW_PROCESS": "新しいプロセス" + } + }, + "APP": { + "DIALOG": { + "START": "開始", + "TITLE": "プロセスの選択", + "LIST": "プロセスリスト", + "ERROR": "Process Services への接続中に問題が発生しました" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/nb.json b/lib/process-services/i18n/nb.json index 14177f7008..aeb93a42de 100644 --- a/lib/process-services/i18n/nb.json +++ b/lib/process-services/i18n/nb.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Prosess", "NAME": "Navn", + "THUMBNAIL": "Miniatyrbilde", "STATUS": "Status", - "END_DATE": "Forfallsdato", + "END_DATE": "Sluttdato", "END_DATE_DEFAULT": "Ingen fristdato", "CATEGORY": "Kategori", "CATEGORY_DEFAULT": "Ingen kategori", @@ -294,5 +295,20 @@ "CREATED-ON": "Opprettet" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Opprett", + "NEW_TASK": "Ny oppgave", + "NEW_PROCESS": "Ny prosess" + } + }, + "APP": { + "DIALOG": { + "START": "Start", + "TITLE": "Velg prosess", + "LIST": "Prosessliste", + "ERROR": "Kan ikke koble til Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/nl.json b/lib/process-services/i18n/nl.json index a3de4aa854..56c2e3b2e7 100644 --- a/lib/process-services/i18n/nl.json +++ b/lib/process-services/i18n/nl.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Proces", "NAME": "Naam", + "THUMBNAIL": "Miniatuur", "STATUS": "Status", - "END_DATE": "Vervaldatum", + "END_DATE": "Einddatum", "END_DATE_DEFAULT": "Geen vervaldatum", "CATEGORY": "Categorie", "CATEGORY_DEFAULT": "Geen categorie", @@ -294,5 +295,20 @@ "CREATED-ON": "Gemaakt op" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Maken", + "NEW_TASK": "Nieuwe taak", + "NEW_PROCESS": "Nieuw proces" + } + }, + "APP": { + "DIALOG": { + "START": "Start", + "TITLE": "Proces selecteren", + "LIST": "Proceslijst", + "ERROR": "Er is een probleem opgetreden bij het verbinden met Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/pt-BR.json b/lib/process-services/i18n/pt-BR.json index 167af3bb4d..beeed0c450 100644 --- a/lib/process-services/i18n/pt-BR.json +++ b/lib/process-services/i18n/pt-BR.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Processo", "NAME": "Nome", + "THUMBNAIL": "Miniatura", "STATUS": "Status", - "END_DATE": "Data de vencimento", + "END_DATE": "Data de término", "END_DATE_DEFAULT": "Nenhuma data", "CATEGORY": "Categoria", "CATEGORY_DEFAULT": "Nenhuma categoria", @@ -294,5 +295,20 @@ "CREATED-ON": "Criado em" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Criar", + "NEW_TASK": "Nova tarefa", + "NEW_PROCESS": "Novo processo" + } + }, + "APP": { + "DIALOG": { + "START": "Iniciar", + "TITLE": "Selecionar Processo", + "LIST": "Lista de processo", + "ERROR": "Houve um problema ao conectar com o Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/ru.json b/lib/process-services/i18n/ru.json index 8af5b810ce..f0689f43b1 100644 --- a/lib/process-services/i18n/ru.json +++ b/lib/process-services/i18n/ru.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "Процесс", "NAME": "Имя", + "THUMBNAIL": "Эскиз", "STATUS": "Статус", - "END_DATE": "Срок", + "END_DATE": "Дата окончания", "END_DATE_DEFAULT": "Нет срока", "CATEGORY": "Категория", "CATEGORY_DEFAULT": "Нет категории", @@ -294,5 +295,20 @@ "CREATED-ON": "Дата создания" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "Создать", + "NEW_TASK": "Новая задача", + "NEW_PROCESS": "Новый процесс" + } + }, + "APP": { + "DIALOG": { + "START": "Начать", + "TITLE": "Выбрать процесс", + "LIST": "Список процессов", + "ERROR": "Возникла проблема при подключении к Process Services" + } } } \ No newline at end of file diff --git a/lib/process-services/i18n/zh-CN.json b/lib/process-services/i18n/zh-CN.json index dd335d5982..0d89bf586c 100644 --- a/lib/process-services/i18n/zh-CN.json +++ b/lib/process-services/i18n/zh-CN.json @@ -176,8 +176,9 @@ "PROPERTIES": { "PROCESS_NAME": "流程", "NAME": "名称", + "THUMBNAIL": "缩略图", "STATUS": "状态", - "END_DATE": "截止日期", + "END_DATE": "结束日期", "END_DATE_DEFAULT": "没有日期", "CATEGORY": "类别", "CATEGORY_DEFAULT": "没有类别", @@ -294,5 +295,20 @@ "CREATED-ON": "创建日期" } } + }, + "ADF_SIDEBAR_ACTION_MENU": { + "BUTTON": { + "CREATE": "创建", + "NEW_TASK": "新任务", + "NEW_PROCESS": "新建流程" + } + }, + "APP": { + "DIALOG": { + "START": "开始", + "TITLE": "选择流程", + "LIST": "流程列表", + "ERROR": "连接到 Process Services 时出现问题" + } } } \ No newline at end of file diff --git a/lib/process-services/material.module.ts b/lib/process-services/material.module.ts index daac2a4345..966f50cf82 100644 --- a/lib/process-services/material.module.ts +++ b/lib/process-services/material.module.ts @@ -21,7 +21,7 @@ import { MatChipsModule, MatDatepickerModule, MatDialogModule, MatGridListModule, MatIconModule, MatInputModule, MatListModule, MatNativeDateModule, MatOptionModule, MatProgressSpinnerModule, MatRadioModule, MatRippleModule, MatSelectModule, MatSlideToggleModule, MatTableModule, MatTabsModule, - MatTooltipModule + MatTooltipModule, MatMenuModule } from '@angular/material'; export function modules() { @@ -30,7 +30,7 @@ export function modules() { MatCheckboxModule, MatDatepickerModule, MatGridListModule, MatIconModule, MatInputModule, MatListModule, MatOptionModule, MatRadioModule, MatSelectModule, MatSlideToggleModule, MatTableModule, MatTabsModule, MatProgressSpinnerModule, MatNativeDateModule, MatRippleModule, MatTooltipModule, - MatChipsModule + MatChipsModule, MatMenuModule ]; } diff --git a/lib/process-services/mock/process/start-process.component.mock.ts b/lib/process-services/mock/process/start-process.component.mock.ts index 6e79388486..667839971e 100644 --- a/lib/process-services/mock/process/start-process.component.mock.ts +++ b/lib/process-services/mock/process/start-process.component.mock.ts @@ -29,7 +29,13 @@ export let testProcessDefRepr = new ProcessDefinitionRepresentation({ hasStartForm: false }); -export let testProcessDefs = [new ProcessDefinitionRepresentation({ +export let testProcessDefinitions = [new ProcessDefinitionRepresentation({ + id: 'my:process1', + name: 'My Process 1', + hasStartForm: false +})]; + +export let testMultipleProcessDefs = [new ProcessDefinitionRepresentation({ id: 'my:process1', name: 'My Process 1', hasStartForm: false diff --git a/lib/process-services/package.json b/lib/process-services/package.json index 5ab70822a9..b53462b74d 100644 --- a/lib/process-services/package.json +++ b/lib/process-services/package.json @@ -1,7 +1,7 @@ { "name": "@alfresco/adf-process-services", "description": "Alfresco ADF process services", - "version": "2.0.0", + "version": "2.1.0", "author": "Alfresco Software, Ltd.", "main": "bundles/adf-process-services.js", "repository": { @@ -12,22 +12,23 @@ "url": "https://github.com/Alfresco/alfresco-ng2-components/issues" }, "dependencies": { - "@angular/animations": "5.0.0", - "@angular/cdk": "5.0.0-rc0", - "@angular/common": "5.0.0", - "@angular/compiler": "5.0.0", - "@angular/core": "5.0.0", - "@angular/flex-layout": "2.0.0-beta.10", - "@angular/forms": "5.0.0", - "@angular/http": "5.0.0", - "@angular/material": "5.0.0-rc0", - "@angular/material-moment-adapter": "^5.0.0-rc0", - "@angular/platform-browser": "5.0.0", - "@angular/platform-browser-dynamic": "5.0.0", - "@angular/router": "5.0.0", + "@angular/animations": "5.1.1", + "@angular/cdk": "5.0.1", + "@angular/common": "5.1.1", + "@angular/compiler": "5.1.1", + "@angular/core": "5.1.1", + "@angular/flex-layout": "2.0.0-beta.12", + "@angular/forms": "5.1.1", + "@angular/http": "5.1.1", + "@angular/material": "5.0.1", + "@angular/material-moment-adapter": "5.0.1", + "@angular/platform-browser": "5.1.1", + "@angular/platform-browser-dynamic": "5.1.1", + "@angular/router": "5.1.1", "@ngx-translate/core": "8.0.0", - "alfresco-js-api": "2.0.0", - "@alfresco/adf-core": "2.0.0", + "alfresco-js-api": "2.1.0", + "@alfresco/adf-core": "2.1.0", + "@alfresco/adf-process-services": "2.1.0", "chart.js": "2.5.0", "core-js": "2.4.1", "hammerjs": "2.0.8", diff --git a/lib/process-services/people/people.component.scss b/lib/process-services/people/people.component.scss index 843dd37c43..27ea5f7099 100644 --- a/lib/process-services/people/people.component.scss +++ b/lib/process-services/people/people.component.scss @@ -51,10 +51,18 @@ padding: 0px; } - adf-people-list ::ng-deep adf-datatable ::ng-deep { + + + adf-people-list adf-datatable { thead { display: none; } + + .adf-data-table .adf-data-table-cell .cell-container { + flex-direction: column; + align-items: left; + } + .people-email { opacity: 0.54; } diff --git a/lib/process-services/process-list/components/process-audit.directive.ts b/lib/process-services/process-list/components/process-audit.directive.ts index 59c8ba26d5..44883d76ed 100644 --- a/lib/process-services/process-list/components/process-audit.directive.ts +++ b/lib/process-services/process-list/components/process-audit.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { ContentService } from '@alfresco/adf-core'; import { Directive, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ProcessService } from './../services/process.service'; diff --git a/lib/process-services/process-list/components/process-instance-header.component.ts b/lib/process-services/process-list/components/process-instance-header.component.ts index 07923d3096..da718fdc17 100644 --- a/lib/process-services/process-list/components/process-instance-header.component.ts +++ b/lib/process-services/process-list/components/process-instance-header.component.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { CardViewDateItemModel, CardViewItem, CardViewTextItemModel } from '@alfresco/adf-core'; +import { CardViewDateItemModel, CardViewItem, CardViewTextItemModel, TranslationService } from '@alfresco/adf-core'; import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { ProcessInstance } from '../models/process-instance.model'; @@ -31,6 +31,9 @@ export class ProcessInstanceHeaderComponent implements OnChanges { properties: CardViewItem []; + constructor(private translationService: TranslationService) { + } + ngOnChanges(changes: SimpleChanges) { this.refreshData(); } @@ -50,28 +53,28 @@ export class ProcessInstanceHeaderComponent implements OnChanges { value: this.processInstance.ended, format: 'MMM DD YYYY', key: 'ended', - default: 'ADF_PROCESS_LIST.PROPERTIES.END_DATE_DEFAULT' + default: this.translationService.instant('ADF_PROCESS_LIST.PROPERTIES.END_DATE_DEFAULT') }), new CardViewTextItemModel( { label: 'ADF_PROCESS_LIST.PROPERTIES.CATEGORY', value: this.processInstance.processDefinitionCategory, key: 'category', - default: 'ADF_PROCESS_LIST.PROPERTIES.CATEGORY_DEFAULT' + default: this.translationService.instant('ADF_PROCESS_LIST.PROPERTIES.CATEGORY_DEFAULT') }), new CardViewTextItemModel( { label: 'ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY', value: this.processInstance.businessKey, key: 'businessKey', - default: 'ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY_DEFAULT' + default: this.translationService.instant('ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY_DEFAULT') }), new CardViewTextItemModel( { label: 'ADF_PROCESS_LIST.PROPERTIES.CREATED_BY', value: this.getStartedByFullName(), key: 'assignee', - default: 'ADF_PROCESS_LIST.PROPERTIES.CREATED_BY_DEFAULT' + default: this.translationService.instant('ADF_PROCESS_LIST.PROPERTIES.CREATED_BY_DEFAULT') }), new CardViewDateItemModel( { @@ -89,7 +92,7 @@ export class ProcessInstanceHeaderComponent implements OnChanges { {label: 'ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION', value: this.processInstance.processDefinitionDescription, key: 'description', - default: 'ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION_DEFAULT' + default: this.translationService.instant('ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION_DEFAULT') }) ]; } diff --git a/lib/process-services/process-list/components/process-instance-tasks.component.ts b/lib/process-services/process-list/components/process-instance-tasks.component.ts index 71de634c58..710a501080 100644 --- a/lib/process-services/process-list/components/process-instance-tasks.component.ts +++ b/lib/process-services/process-list/components/process-instance-tasks.component.ts @@ -53,9 +53,6 @@ export class ProcessInstanceTasksComponent implements OnInit, OnChanges { message: string; processId: string; - // @ViewChild('dialog') - // dialog: any; - @ViewChild('startDialog') startDialog: any; diff --git a/lib/process-services/process-list/components/process-list.component.css b/lib/process-services/process-list/components/process-list.component.css index 39467dee0c..85ccfdaab1 100644 --- a/lib/process-services/process-list/components/process-list.component.css +++ b/lib/process-services/process-list/components/process-list.component.css @@ -1,13 +1,13 @@ -adf-datatable >>> .column-header { +adf-datatable ::ng-deep .column-header { color: #232323; font-size: 15px; } -adf-datatable >>> .data-cell { +adf-datatable ::ng-deep .data-cell { cursor: pointer !important; } -adf-datatable >>> .cell-value{ +adf-datatable ::ng-deep .cell-value{ width: 250px; white-space: nowrap; overflow: hidden; diff --git a/lib/process-services/process-list/components/start-process.component.html b/lib/process-services/process-list/components/start-process.component.html index 3203e9aa1d..8d8952a0c1 100644 --- a/lib/process-services/process-list/components/start-process.component.html +++ b/lib/process-services/process-list/components/start-process.component.html @@ -8,17 +8,22 @@ <mat-form-field class="adf-process-input-container"> <input matInput placeholder="{{'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.NAME'|translate}}" [(ngModel)]="name" id="processName" required /> </mat-form-field> - <mat-form-field> - <mat-select placeholder="{{'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.TYPE'|translate}}" [(ngModel)]="currentProcessDef.id" (ngModelChange)="onProcessDefChange($event)" required> - <mat-option>{{'ADF_PROCESS_LIST.START_PROCESS.FORM.TYPE_PLACEHOLDER' | translate}}</mat-option> - <mat-option *ngFor="let processDef of processDefinitions" [value]="processDef.id"> - {{ processDef.name }} - </mat-option> - </mat-select> - </mat-form-field> + + <div *ngIf="showSelectProcessDropdown"> + <mat-form-field> + <mat-select [(value)]="selectedProcessDef" placeholder="{{'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.TYPE'|translate}}" required> + <mat-option *ngIf="!hasSingleProcessDefinition()">{{'ADF_PROCESS_LIST.START_PROCESS.FORM.TYPE_PLACEHOLDER' | translate}}</mat-option> + <mat-option *ngFor="let processDef of processDefinitions" [value]="processDef"> + {{ processDef.name }} + </mat-option> + </mat-select> + </mat-form-field> + </div> + <adf-start-form *ngIf="hasStartForm()" + [data]="values" [disableStartProcessButton]="!hasProcessName()" - [processDefinitionId]="currentProcessDef.id" + [processDefinitionId]="selectedProcessDef.id" (outcomeClick)="onOutcomeClick($event)" [showRefreshButton]="false"> <button form-custom-button mat-button (click)="cancelStartProcess()" id="cancle_process" class=""> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.CANCEL'| translate}} </button> @@ -31,6 +36,6 @@ </mat-card-content> <mat-card-actions *ngIf="!hasStartForm()"> <button mat-button *ngIf="!hasStartForm()" (click)="cancelStartProcess()" id="cancle_process" class=""> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.CANCEL'| translate}} </button> - <button mat-button *ngIf="!hasStartForm()" [disabled]="!validateForm()" (click)="startProcess()" data-automation-id="btn-start" id="button-start" class="btn-start"> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.START' | translate}} </button> + <button color="primary" mat-button *ngIf="!hasStartForm()" [disabled]="!validateForm()" (click)="startProcess()" data-automation-id="btn-start" id="button-start" class="btn-start"> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.START' | translate}} </button> </mat-card-actions> </mat-card> diff --git a/lib/process-services/process-list/components/start-process.component.scss b/lib/process-services/process-list/components/start-process.component.scss index 251dfb070e..b95df69d10 100644 --- a/lib/process-services/process-list/components/start-process.component.scss +++ b/lib/process-services/process-list/components/start-process.component.scss @@ -1,8 +1,8 @@ .adf { &-start-process { - width: calc(66.6666% - 48px); - margin-left: calc(33.3333333333% / 2); - margin-right: calc(33.3333333333% / 2); + width: 66%; + margin-left: auto; + margin-right: auto; margin-top: 10px; .mat-select-trigger { font-size: 14px !important; @@ -16,6 +16,9 @@ } mat-card-actions { text-align: right; + .mat-button { + text-transform: uppercase !important; + } } } &-process-input-container { @@ -32,4 +35,12 @@ &-start-form-actions { text-align: right !important; } -} \ No newline at end of file +} + +@media (max-width: 600px) { + .adf-start-process { + width: 90%; + margin-left: auto; + margin-right: auto; + } +} diff --git a/lib/process-services/process-list/components/start-process.component.spec.ts b/lib/process-services/process-list/components/start-process.component.spec.ts index 3e5c6fa833..ce8b1b9dda 100644 --- a/lib/process-services/process-list/components/start-process.component.spec.ts +++ b/lib/process-services/process-list/components/start-process.component.spec.ts @@ -23,16 +23,25 @@ import { MatInputModule, MatSelectModule } from '@angular/material'; -import { FormModule, FormService } from '@alfresco/adf-core'; +import { ActivitiContentService, AppConfigService, FormModule, FormService } from '@alfresco/adf-core'; import { Observable } from 'rxjs/Observable'; import { ProcessInstanceVariable } from '../models/process-instance-variable.model'; import { ProcessService } from '../services/process.service'; -import { newProcess, taskFormMock, testProcessDefRepr, testProcessDefs, testProcessDefWithForm } from '../../mock'; +import { + newProcess, + taskFormMock, + testProcessDefRepr, + testMultipleProcessDefs, + testProcessDefWithForm, + testProcessDefinitions +} from '../../mock'; import { StartProcessInstanceComponent } from './start-process.component'; -describe('StartProcessInstanceComponent', () => { +describe('StartFormComponent', () => { + let appConfig: AppConfigService; + let activitiContentService: ActivitiContentService; let component: StartProcessInstanceComponent; let fixture: ComponentFixture<StartProcessInstanceComponent>; let processService: ProcessService; @@ -54,6 +63,7 @@ describe('StartProcessInstanceComponent', () => { StartProcessInstanceComponent ], providers: [ + ActivitiContentService, ProcessService, FormService ] @@ -61,51 +71,158 @@ describe('StartProcessInstanceComponent', () => { })); beforeEach(() => { - + appConfig = TestBed.get(AppConfigService); + activitiContentService = TestBed.get(ActivitiContentService); fixture = TestBed.createComponent(StartProcessInstanceComponent); component = fixture.componentInstance; processService = fixture.debugElement.injector.get(ProcessService); formService = fixture.debugElement.injector.get(FormService); - getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(Observable.of(testProcessDefs)); + getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(Observable.of(testMultipleProcessDefs)); startProcessSpy = spyOn(processService, 'startProcess').and.returnValue(Observable.of(newProcess)); getStartFormDefinitionSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(Observable.of(taskFormMock)); - + spyOn(activitiContentService, 'applyAlfrescoNode').and.returnValue(Observable.of({ id: 1234 })); }); it('should create instance of StartProcessInstanceComponent', () => { expect(fixture.componentInstance instanceof StartProcessInstanceComponent).toBe(true, 'should create StartProcessInstanceComponent'); }); + describe('first step', () => { + + describe('without start form', () => { + + beforeEach(() => { + component.name = 'My new process'; + let change = new SimpleChange(null, 123, true); + component.ngOnChanges({ 'appId': change }); + }); + + it('should enable start button when name and process filled out', async(() => { + component.selectedProcessDef = testProcessDefRepr; + fixture.detectChanges(); + fixture.whenStable().then(() => { + let startBtn = fixture.nativeElement.querySelector('#button-start'); + expect(startBtn.disabled).toBe(false); + }); + })); + + it('should have start button disabled when name not filled out', async(() => { + component.name = ''; + fixture.detectChanges(); + fixture.whenStable().then(() => { + let startBtn = fixture.nativeElement.querySelector('#button-start'); + expect(startBtn.disabled).toBe(true); + }); + })); + + it('should have start button disabled when no process is selected', async(() => { + component.selectedProcessDef = null; + fixture.detectChanges(); + fixture.whenStable().then(() => { + let startBtn = fixture.nativeElement.querySelector('#button-start'); + expect(startBtn.disabled).toBe(true); + }); + })); + }); + + describe('with start form', () => { + + beforeEach(() => { + getDefinitionsSpy.and.returnValue(Observable.of(testProcessDefWithForm)); + let change = new SimpleChange(null, 123, true); + component.ngOnChanges({ 'appId': change }); + }); + + it('should initialize start form', async(() => { + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(component.startForm).toBeDefined(); + expect(component.startForm).not.toBeNull(); + }); + })); + + it('should load start form from service', async(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(getStartFormDefinitionSpy).toHaveBeenCalled(); + }); + })); + + it('should have start button disabled if the process is not seleted', async(() => { + component.name = 'My new process'; + fixture.detectChanges(); + fixture.whenStable().then(() => { + let startBtn = fixture.nativeElement.querySelector('#button-start'); + expect(startBtn).toBeNull(); + }); + })); + + it('should emit cancel event on cancel Button', async(() => { + fixture.detectChanges(); + let cancelButton = fixture.nativeElement.querySelector('#cancle_process'); + let cancelSpy: jasmine.Spy = spyOn(component.cancel, 'emit'); + cancelButton.click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(cancelSpy).toHaveBeenCalled(); + }); + })); + }); + + describe('CS content connection', () => { + + it('alfrescoRepositoryName default configuration property', () => { + expect(component.getAlfrescoRepositoryName()).toBe('alfresco-1Alfresco'); + }); + + it('alfrescoRepositoryName configuration property should be fetched', () => { + appConfig.config = Object.assign(appConfig.config, { + 'alfrescoRepositoryName': 'alfresco-123' + }; + + expect(component.getAlfrescoRepositoryName()).toBe('alfresco-123Alfresco'); + }); + + it('if values in input is a node should be linked in the process service', async(() => { + + component.values = {}; + component.values['file'] = { + isFile: true, + name= 'example-file' + }; + + component.moveNodeFromCStoPS(); + + fixture.whenStable().then(() => { + expect(component.values.file[0].id).toBe(1234); + }); + })); + }); + }); + describe('process definitions list', () => { it('should call service to fetch process definitions with appId', () => { - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); fixture.detectChanges(); - expect(getDefinitionsSpy).toHaveBeenCalledWith('123'); - }); - - it('should call service to fetch process definitions without appId', () => { - let change = new SimpleChange(null, null, true); - component.ngOnChanges({'appId': change}); - fixture.detectChanges(); - - expect(getDefinitionsSpy).toHaveBeenCalledWith(null); + expect(getDefinitionsSpy).toHaveBeenCalledWith(123); }); it('should call service to fetch process definitions with appId when provided', () => { - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); fixture.detectChanges(); - expect(getDefinitionsSpy).toHaveBeenCalledWith('123'); + expect(getDefinitionsSpy).toHaveBeenCalledWith(123); }); it('should display the correct number of processes in the select list', () => { - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); fixture.detectChanges(); let selectElement = fixture.nativeElement.querySelector('mat-select'); @@ -113,9 +230,9 @@ describe('StartProcessInstanceComponent', () => { }); it('should display the option def details', () => { - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); - component.processDefinitions = testProcessDefs; + component.appId = 123; + component.ngOnChanges({}); + component.processDefinitions = testMultipleProcessDefs; fixture.detectChanges(); fixture.whenStable().then(() => { let selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger'); @@ -130,8 +247,8 @@ describe('StartProcessInstanceComponent', () => { it('should indicate an error to the user if process defs cannot be loaded', async(() => { getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.throw({})); - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -143,8 +260,8 @@ describe('StartProcessInstanceComponent', () => { it('should show no process available message when no process definition is loaded', async(() => { getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of([])); - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); fixture.detectChanges(); fixture.whenStable().then(() => { @@ -154,12 +271,71 @@ describe('StartProcessInstanceComponent', () => { }); })); + it('should select processDefinition based on processDefinition input', async(() => { + getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of(testMultipleProcessDefs)); + component.appId = 123; + component.processDefinitionName = 'My Process 2'; + component.ngOnChanges({}); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(component.selectedProcessDef.name).toBe(JSON.parse(JSON.stringify(testMultipleProcessDefs[1])).name); + }); + })); + + it('should select automatically the processDefinition if the app contain oly one', async(() => { + getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of(testProcessDefinitions)); + component.appId = 123; + component.ngOnChanges({}); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(component.selectedProcessDef.name).toBe(JSON.parse(JSON.stringify(testProcessDefinitions[0])).name); + }); + })); + + describe('dropdown', () => { + + it('should hide the process dropdown if showSelectProcessDropdown is false', async(() => { + getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of([testProcessDefRepr])); + component.appId = 123; + component.showSelectProcessDropdown = false; + component.ngOnChanges({}); + fixture.detectChanges(); + fixture.whenStable().then(() => { + let selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger'); + expect(selectElement).toBeNull(); + }); + })); + + it('should show the process dropdown if showSelectProcessDropdown is false', async(() => { + getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of(testMultipleProcessDefs)); + component.appId = 123; + component.processDefinitionName = 'My Process 2'; + component.showSelectProcessDropdown = true; + component.ngOnChanges({}); + fixture.detectChanges(); + fixture.whenStable().then(() => { + let selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger'); + expect(selectElement).not.toBeNull(); + }); + })); + + it('should show the process dropdown by default', async(() => { + getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of(testMultipleProcessDefs)); + component.appId = 123; + component.processDefinitionName = 'My Process 2'; + component.ngOnChanges({}); + fixture.detectChanges(); + fixture.whenStable().then(() => { + let selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger'); + expect(selectElement).not.toBeNull(); + }); + })); + }); }); describe('input changes', () => { let change = new SimpleChange(123, 456, true); - let nullChange = new SimpleChange(123, null, true); beforeEach(async(() => { component.appId = 123; @@ -170,22 +346,20 @@ describe('StartProcessInstanceComponent', () => { }); })); - it('should reload processes when appId input changed', () => { - component.ngOnChanges({appId: change}); - expect(getDefinitionsSpy).toHaveBeenCalledWith(456); - }); - - it('should reload processes when appId input changed to null', () => { - component.ngOnChanges({appId: nullChange}); - expect(getDefinitionsSpy).toHaveBeenCalledWith(null); - }); + it('should reload processes when appId input changed', async(() => { + component.appId = 456; + component.ngOnChanges({ appId: change }); + fixture.whenStable().then(() => { + expect(getDefinitionsSpy).toHaveBeenCalledWith(456); + }); + })); it('should get current processDeff', () => { - component.ngOnChanges({appId: change}); - component.onProcessDefChange('my:Process'); + component.appId = 456; + component.ngOnChanges({ appId: change }); fixture.detectChanges(); expect(getDefinitionsSpy).toHaveBeenCalled(); - expect(component.processDefinitions).toBe(testProcessDefs); + expect(component.processDefinitions).toBe(testMultipleProcessDefs); }); }); @@ -193,12 +367,12 @@ describe('StartProcessInstanceComponent', () => { beforeEach(() => { component.name = 'My new process'; - let change = new SimpleChange(null, 123, true); - component.ngOnChanges({'appId': change}); + component.appId = 123; + component.ngOnChanges({}); }); it('should call service to start process if required fields provided', async(() => { - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { expect(startProcessSpy).toHaveBeenCalled(); @@ -214,7 +388,7 @@ describe('StartProcessInstanceComponent', () => { })); it('should call service to start process with the correct parameters', async(() => { - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined, undefined, undefined); @@ -231,7 +405,7 @@ describe('StartProcessInstanceComponent', () => { inputProcessVariable.push(variable); component.variables = inputProcessVariable; - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined, undefined, inputProcessVariable); @@ -240,7 +414,7 @@ describe('StartProcessInstanceComponent', () => { it('should output start event when process started successfully', async(() => { let emitSpy = spyOn(component.start, 'emit'); - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { expect(emitSpy).toHaveBeenCalledWith(newProcess); @@ -249,9 +423,9 @@ describe('StartProcessInstanceComponent', () => { it('should throw error event when process cannot be started', async(() => { let errorSpy = spyOn(component.error, 'error'); - let error = {message: 'My error'}; + let error = { message: 'My error' }; startProcessSpy = startProcessSpy.and.returnValue(Observable.throw(error)); - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { expect(errorSpy).toHaveBeenCalledWith(error); @@ -260,7 +434,7 @@ describe('StartProcessInstanceComponent', () => { it('should indicate an error to the user if process cannot be started', async(() => { startProcessSpy = startProcessSpy.and.returnValue(Observable.throw({})); - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; component.startProcess(); fixture.whenStable().then(() => { fixture.detectChanges(); @@ -270,16 +444,38 @@ describe('StartProcessInstanceComponent', () => { }); })); - it('should emit start event when start the process with currentProcessDef and name', () => { + it('should emit start event when start select a process and add a name', async(() => { let startSpy: jasmine.Spy = spyOn(component.start, 'emit'); - component.currentProcessDef.id = '1001'; + component.selectedProcessDef = testProcessDefRepr; component.name = 'my:Process'; component.startProcess(); fixture.detectChanges(); - expect(startSpy).toHaveBeenCalled(); + fixture.whenStable().then(() => { + let startButton = fixture.nativeElement.querySelector('#button-start'); + startButton.click(); + expect(startSpy).toHaveBeenCalled(); + }); + })); + + it('should not emit start event when start the process without select a process and name', () => { + component.name = null; + component.selectedProcessDef = null; + let startSpy: jasmine.Spy = spyOn(component.start, 'emit'); + component.startProcess(); + fixture.detectChanges(); + expect(startSpy).not.toHaveBeenCalled(); }); - it('should not emit start event when start the process without currentProcessDef and name', () => { + it('should not emit start event when start the process without name', () => { + component.name = null; + let startSpy: jasmine.Spy = spyOn(component.start, 'emit'); + component.startProcess(); + fixture.detectChanges(); + expect(startSpy).not.toHaveBeenCalled(); + }); + + it('should not emit start event when start the process without select a process', () => { + component.selectedProcessDef = null; let startSpy: jasmine.Spy = spyOn(component.start, 'emit'); component.startProcess(); fixture.detectChanges(); @@ -289,7 +485,7 @@ describe('StartProcessInstanceComponent', () => { it('should able to start the process when the required fields are filled up', async(() => { let startSpy: jasmine.Spy = spyOn(component.start, 'emit'); component.name = 'my:process1'; - component.onProcessDefChange('my:process1'); + component.selectedProcessDef = testProcessDefRepr; fixture.detectChanges(); fixture.whenStable().then(() => { let startButton = fixture.nativeElement.querySelector('#button-start'); @@ -299,9 +495,9 @@ describe('StartProcessInstanceComponent', () => { })); it('should return true if startFrom defined', async(() => { - component.currentProcessDef = testProcessDefRepr; + component.selectedProcessDef = testProcessDefRepr; component.name = 'my:process1'; - component.currentProcessDef.hasStartForm = true; + component.selectedProcessDef.hasStartForm = true; component.hasStartForm(); fixture.whenStable().then(() => { expect(component.hasStartForm()).toBe(true); @@ -310,85 +506,4 @@ describe('StartProcessInstanceComponent', () => { }); - describe('start forms', () => { - - let startBtn; - - describe('without start form', () => { - - beforeEach(async(() => { - component.name = 'My new process'; - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); - fixture.detectChanges(); - component.onProcessDefChange('my:process1'); - fixture.whenStable(); - startBtn = fixture.nativeElement.querySelector('#button-start'); - })); - - it('should have start button disabled when name not filled out', async(() => { - component.name = ''; - fixture.detectChanges(); - expect(startBtn.disabled).toBe(true); - })); - - it('should have start button disabled when no process is selected', async(() => { - component.onProcessDefChange(''); - fixture.detectChanges(); - expect(startBtn.disabled).toBe(true); - })); - - it('should enable start button when name and process filled out', async(() => { - fixture.detectChanges(); - let startButton = fixture.nativeElement.querySelector('#button-start'); - expect(startButton.disabled).toBeFalsy(); - })); - - it('should disable the start process button when process name is empty', async(() => { - component.name = ''; - fixture.detectChanges(); - let startButton = fixture.nativeElement.querySelector('#button-start'); - expect(startButton.disabled).toBeTruthy(); - })); - - }); - - describe('with start form', () => { - - beforeEach(() => { - getDefinitionsSpy.and.returnValue(Observable.of(testProcessDefWithForm)); - let change = new SimpleChange(null, '123', true); - component.ngOnChanges({'appId': change}); - component.onProcessDefChange('my:process1'); - fixture.detectChanges(); - fixture.whenStable(); - startBtn = fixture.nativeElement.querySelector('#button-start'); - }); - - it('should initialize start form', () => { - expect(component.startForm).toBeDefined(); - expect(component.startForm).not.toBeNull(); - }); - - it('should load start form from service', () => { - expect(getStartFormDefinitionSpy).toHaveBeenCalled(); - }); - - it('should not show the start process button', async(() => { - component.name = 'My new process'; - fixture.detectChanges(); - expect(startBtn).toBeNull(); - })); - - it('should emit cancel event on cancel Button', () => { - let cancelButton = fixture.nativeElement.querySelector('#cancle_process'); - let cancelSpy: jasmine.Spy = spyOn(component.cancel, 'emit'); - cancelButton.click(); - fixture.detectChanges(); - expect(cancelSpy).toHaveBeenCalled(); - }); - }); - - }); - }); diff --git a/lib/process-services/process-list/components/start-process.component.ts b/lib/process-services/process-list/components/start-process.component.ts index 2c16a45acf..d08fda65ea 100644 --- a/lib/process-services/process-list/components/start-process.component.ts +++ b/lib/process-services/process-list/components/start-process.component.ts @@ -15,12 +15,28 @@ * limitations under the License. */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; -import { StartFormComponent } from '@alfresco/adf-core'; +import { + Component, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { + ActivitiContentService, + AppConfigService, + StartFormComponent, + FormRenderingService, + FormValues +} from '@alfresco/adf-core'; import { ProcessInstanceVariable } from '../models/process-instance-variable.model'; import { ProcessDefinitionRepresentation } from './../models/process-definition.model'; import { ProcessInstance } from './../models/process-instance.model'; import { ProcessService } from './../services/process.service'; +import { AttachFileWidgetComponent, AttachFolderWidgetComponent } from '../../content-widget'; @Component({ selector: 'adf-start-process', @@ -33,9 +49,21 @@ export class StartProcessInstanceComponent implements OnChanges { @Input() appId: number; + @Input() + processDefinitionName: string; + @Input() variables: ProcessInstanceVariable[]; + @Input() + values: FormValues; + + @Input() + name: string; + + @Input() + showSelectProcessDropdown: boolean = true; + @Output() start: EventEmitter<ProcessInstance> = new EventEmitter<ProcessInstance>(); @@ -50,39 +78,81 @@ export class StartProcessInstanceComponent implements OnChanges { processDefinitions: ProcessDefinitionRepresentation[] = []; - name: string; - - currentProcessDef: ProcessDefinitionRepresentation = new ProcessDefinitionRepresentation(); + selectedProcessDef: ProcessDefinitionRepresentation = new ProcessDefinitionRepresentation(); errorMessageId: string = ''; - constructor(private activitiProcess: ProcessService) { + constructor(private activitiProcess: ProcessService, + private formRenderingService: FormRenderingService, + private activitiContentService: ActivitiContentService, + private appConfig: AppConfigService) { + this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true); + this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true); } ngOnChanges(changes: SimpleChanges) { - let appIdChange = changes['appId']; - let appId = appIdChange ? appIdChange.currentValue : null; - this.load(appId); + if (changes['values'] && changes['values'].currentValue) { + this.moveNodeFromCStoPS(); + } + + this.loadStartProcess(); } - public load(appId?: number) { + public loadStartProcess() { this.resetSelectedProcessDefinition(); this.resetErrorMessage(); - this.activitiProcess.getProcessDefinitions(appId).subscribe( - (res) => { - this.processDefinitions = res; + + this.activitiProcess.getProcessDefinitions(this.appId).subscribe( + (processDefinitionRepresentations: ProcessDefinitionRepresentation[]) => { + this.processDefinitions = processDefinitionRepresentations; + + if (this.hasSingleProcessDefinition()) { + this.selectedProcessDef = this.processDefinitions[0]; + } else { + this.selectedProcessDef = this.processDefinitions.find((currentProcessDefinition) => { + return currentProcessDefinition.name === this.processDefinitionName; + }); + } }, () => { this.errorMessageId = 'ADF_PROCESS_LIST.START_PROCESS.ERROR.LOAD_PROCESS_DEFS'; + }); + + } + + hasSingleProcessDefinition(): boolean { + return this.processDefinitions.length === 1; + } + + getAlfrescoRepositoryName(): string { + let alfrescoRepositoryName = this.appConfig.get<string>('alfrescoRepositoryName'); + if (!alfrescoRepositoryName) { + alfrescoRepositoryName = 'alfresco-1'; + } + return alfrescoRepositoryName + 'Alfresco'; + } + + moveNodeFromCStoPS() { + let accountIdentifier = this.getAlfrescoRepositoryName(); + + for (let key in this.values) { + if (this.values.hasOwnProperty(key)) { + let currentValue = this.values[key]; + + if (currentValue.isFile) { + this.activitiContentService.applyAlfrescoNode(currentValue, null, accountIdentifier).subscribe((res) => { + this.values[key] = [res]; + }); + } } - ); + } } public startProcess(outcome?: string) { - if (this.currentProcessDef.id && this.name) { + if (this.selectedProcessDef && this.selectedProcessDef.id && this.name) { this.resetErrorMessage(); let formValues = this.startForm ? this.startForm.form.values : undefined; - this.activitiProcess.startProcess(this.currentProcessDef.id, this.name, outcome, formValues, this.variables).subscribe( + this.activitiProcess.startProcess(this.selectedProcessDef.id, this.name, outcome, formValues, this.variables).subscribe( (res) => { this.name = ''; this.start.emit(res); @@ -95,30 +165,19 @@ export class StartProcessInstanceComponent implements OnChanges { } } - onProcessDefChange(processDefinitionId) { - let processDef = this.processDefinitions.find((processDefinition) => { - return processDefinition.id === processDefinitionId; - }); - if (processDef) { - this.currentProcessDef = JSON.parse(JSON.stringify(processDef)); - } else { - this.resetSelectedProcessDefinition(); - } - } - public cancelStartProcess() { this.cancel.emit(); } - hasStartForm() { - return this.currentProcessDef && this.currentProcessDef.hasStartForm; + hasStartForm(): boolean { + return this.selectedProcessDef && this.selectedProcessDef.hasStartForm; } isProcessDefinitionEmpty() { return this.processDefinitions ? (this.processDefinitions.length > 0 || this.errorMessageId) : this.errorMessageId; } - isStartFormMissingOrValid() { + isStartFormMissingOrValid(): boolean { if (this.startForm) { return this.startForm.form && this.startForm.form.isValid; } else { @@ -126,19 +185,19 @@ export class StartProcessInstanceComponent implements OnChanges { } } - validateForm() { - return this.currentProcessDef.id && this.name && this.isStartFormMissingOrValid(); + validateForm(): boolean { + return this.selectedProcessDef && this.selectedProcessDef.id && this.name && this.isStartFormMissingOrValid(); } private resetSelectedProcessDefinition() { - this.currentProcessDef = new ProcessDefinitionRepresentation(); + this.selectedProcessDef = new ProcessDefinitionRepresentation(); } private resetErrorMessage(): void { this.errorMessageId = ''; } - hasErrorMessage() { + hasErrorMessage(): boolean { return this.processDefinitions.length === 0 && !this.errorMessageId; } diff --git a/lib/process-services/process-list/process-list.module.ts b/lib/process-services/process-list/process-list.module.ts index 86e59a5022..6dc219970d 100644 --- a/lib/process-services/process-list/process-list.module.ts +++ b/lib/process-services/process-list/process-list.module.ts @@ -27,6 +27,7 @@ import { CardViewModule, CardViewUpdateService, DataColumnModule, DataTableModul import { TaskListModule } from '../task-list/task-list.module'; import { PeopleModule } from '../people/people.module'; import { CommentsModule } from '../comments/comments.module'; +import { ContentWidgetModule } from '../content-widget/content-widget.module'; import { ProcessAuditDirective } from './components/process-audit.directive'; import { ProcessFiltersComponent } from './components/process-filters.component'; @@ -55,7 +56,8 @@ import { ProcessFilterService } from './services/process-filter.service'; DataColumnModule, DirectiveModule, PeopleModule, - CommentsModule + CommentsModule, + ContentWidgetModule ], declarations: [ ProcessInstanceListComponent, diff --git a/lib/process-services/process-list/services/process-filter.service.spec.ts b/lib/process-services/process-list/services/process-filter.service.spec.ts index 0485eeabde..3464f26725 100644 --- a/lib/process-services/process-list/services/process-filter.service.spec.ts +++ b/lib/process-services/process-list/services/process-filter.service.spec.ts @@ -23,6 +23,8 @@ import { mockError, fakeProcessFilters } from '../../mock'; import { FilterProcessRepresentationModel } from '../models/filter-process.model'; import { ProcessFilterService } from './process-filter.service'; +declare let jasmine: any; + describe('Process filter', () => { let service: ProcessFilterService; @@ -51,9 +53,11 @@ describe('Process filter', () => { .and .returnValue(Promise.resolve(fakeProcessFilters)); - createFilter = spyOn(alfrescoApi.activiti.userFiltersApi, 'createUserProcessInstanceFilter') - .and - .callFake((filter: FilterProcessRepresentationModel) => Promise.resolve(filter)); + jasmine.Ajax.install(); + }); + + afterEach(() => { + jasmine.Ajax.uninstall(); }); describe('get filters', () => { @@ -103,16 +107,41 @@ describe('Process filter', () => { })); it('should return the default filters', (done) => { - service.createDefaultFilters(1234).subscribe( - (res: FilterProcessRepresentationModel []) => { - expect(res).toBeDefined(); - expect(res.length).toEqual(3); - expect(res[0].name).toEqual('Running'); - expect(res[1].name).toEqual('Completed'); - expect(res[2].name).toEqual('All'); - done(); - } - ); + service.createDefaultFilters(1234).subscribe((res: FilterProcessRepresentationModel []) => { + expect(res).toBeDefined(); + expect(res.length).toEqual(3); + expect(res[0].name).toEqual('Running'); + expect(res[0].id).toEqual(111); + expect(res[1].name).toEqual('Completed'); + expect(res[1].id).toEqual(222); + expect(res[2].name).toEqual('All'); + expect(res[2].id).toEqual(333); + done(); + }); + + jasmine.Ajax.requests.at(0).respondWith({ + 'status': 200, + contentType: 'application/json', + responseText: JSON.stringify({ + appId: 1001, id: '111', name: 'Running', icon: 'fake-icon', recent: false + }) + }); + + jasmine.Ajax.requests.at(1).respondWith({ + 'status': 200, + contentType: 'application/json', + responseText: JSON.stringify({ + appId: 1001, id: '222', name: 'Completed', icon: 'fake-icon', recent: false + }) + }); + + jasmine.Ajax.requests.at(2).respondWith({ + 'status': 200, + contentType: 'application/json', + responseText: JSON.stringify({ + appId: 1001, id: '333', name: 'All', icon: 'fake-icon', recent: false + }) + }); }); it('should pass on any error that is returned by the API', async(() => { @@ -130,6 +159,12 @@ describe('Process filter', () => { describe('add filter', () => { + beforeEach(() => { + createFilter = spyOn(alfrescoApi.activiti.userFiltersApi, 'createUserProcessInstanceFilter') + .and + .callFake((processfilter: FilterProcessRepresentationModel) => Promise.resolve(processfilter)); + }); + let filter = fakeProcessFilters.data[0]; it('should call the API to create the filter', () => { diff --git a/lib/process-services/process-list/services/process-filter.service.ts b/lib/process-services/process-list/services/process-filter.service.ts index 12950df71c..12dde7113b 100644 --- a/lib/process-services/process-list/services/process-filter.service.ts +++ b/lib/process-services/process-list/services/process-filter.service.ts @@ -91,10 +91,13 @@ export class ProcessFilterService { let filters: FilterProcessRepresentationModel[] = []; res.forEach((filter) => { if (filter.name === runningFilter.name) { + runningFilter.id = filter.id; filters.push(runningFilter); } else if (filter.name === completedFilter.name) { + completedFilter.id = filter.id; filters.push(completedFilter); } else if (filter.name === allFilter.name) { + allFilter.id = filter.id; filters.push(allFilter); } }); diff --git a/lib/process-services/process-list/services/process.service.ts b/lib/process-services/process-list/services/process.service.ts index 3239cb1249..b087c37587 100644 --- a/lib/process-services/process-list/services/process.service.ts +++ b/lib/process-services/process-list/services/process.service.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AlfrescoApiService } from '@alfresco/adf-core'; +import { AlfrescoApiService, FormValues } from '@alfresco/adf-core'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { TaskDetailsModel } from '../../task-list'; @@ -98,7 +98,7 @@ export class ProcessService { .catch(err => this.handleProcessError(err)); } - startProcess(processDefinitionId: string, name: string, outcome?: string, startFormValues?: any, variables?: ProcessInstanceVariable[]): Observable<ProcessInstance> { + startProcess(processDefinitionId: string, name: string, outcome?: string, startFormValues?: FormValues, variables?: ProcessInstanceVariable[]): Observable<ProcessInstance> { let startRequest: any = { name: name, processDefinitionId: processDefinitionId diff --git a/lib/process-services/process.module.ts b/lib/process-services/process.module.ts index aeb531d674..0299b5b1fa 100644 --- a/lib/process-services/process.module.ts +++ b/lib/process-services/process.module.ts @@ -65,8 +65,7 @@ import { PeopleModule } from './people/people.module'; TaskListModule, AppsListModule, AttachmentModule, - PeopleModule, - MaterialModule + PeopleModule ] }) export class ProcessModule { diff --git a/lib/process-services/task-list/components/checklist.component.ts b/lib/process-services/task-list/components/checklist.component.ts index adf8fc2e84..02283bc543 100644 --- a/lib/process-services/task-list/components/checklist.component.ts +++ b/lib/process-services/task-list/components/checklist.component.ts @@ -30,21 +30,31 @@ import { TaskListService } from './../services/tasklist.service'; }) export class ChecklistComponent implements OnInit, OnChanges { + /** (required) The id of the parent task to which subtasks are + * attached. + */ @Input() taskId: string; + /** Toggle readonly state of the form. All form widgets + * will render as readonly if enabled. + */ @Input() readOnly: boolean = false; + /** (required) The assignee id that the subtasks are assigned to. */ @Input() assignee: string; + /** Emitted when a new checklist task is created. */ @Output() checklistTaskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>(); + /** Emitted when a checklist task is deleted. */ @Output() checklistTaskDeleted: EventEmitter<string> = new EventEmitter<string>(); + /** Emitted when an error occurs. */ @Output() error: EventEmitter<any> = new EventEmitter<any>(); diff --git a/lib/process-services/task-list/components/no-task-detail-template.directive.spec.ts b/lib/process-services/task-list/components/no-task-detail-template.directive.spec.ts index f44a78655f..f74a7785c2 100644 --- a/lib/process-services/task-list/components/no-task-detail-template.directive.spec.ts +++ b/lib/process-services/task-list/components/no-task-detail-template.directive.spec.ts @@ -17,6 +17,7 @@ import { NoTaskDetailsTemplateDirective } from './no-task-detail-template.directive'; import { TaskDetailsComponent } from './task-details.component'; +import { FormRenderingService } from '@alfresco/adf-core'; describe('NoTaskDetailsTemplateDirective', () => { @@ -24,7 +25,7 @@ describe('NoTaskDetailsTemplateDirective', () => { let detailsComponent: TaskDetailsComponent; beforeEach(() => { - detailsComponent = new TaskDetailsComponent(null, null, null, null, null, null); + detailsComponent = new TaskDetailsComponent(null, null, null, new FormRenderingService(), null, null, null); component = new NoTaskDetailsTemplateDirective(detailsComponent); }); diff --git a/lib/process-services/task-list/components/start-task.component.html b/lib/process-services/task-list/components/start-task.component.html index 8da846fdd3..e16230fda2 100644 --- a/lib/process-services/task-list/components/start-task.component.html +++ b/lib/process-services/task-list/components/start-task.component.html @@ -74,7 +74,7 @@ <button mat-button (click)="onCancel()" id="button-cancle"> {{'ADF_TASK_LIST.START_TASK.FORM.ACTION.CANCEL'|translate}} </button> - <button mat-button [disabled]="!startTaskmodel.name || dateError" (click)="start()" id="button-start"> + <button color="primary" mat-button [disabled]="!startTaskmodel.name || dateError" (click)="start()" id="button-start"> {{'ADF_TASK_LIST.START_TASK.FORM.ACTION.START'|translate}} </button> </div> diff --git a/lib/process-services/task-list/components/start-task.component.scss b/lib/process-services/task-list/components/start-task.component.scss index eeb352e272..6aa9d0d25e 100644 --- a/lib/process-services/task-list/components/start-task.component.scss +++ b/lib/process-services/task-list/components/start-task.component.scss @@ -14,9 +14,9 @@ } .adf-new-task-layout-card { - width: 66.6667%; - margin-right: calc(33.3333% / 2 - 24px); - margin-left: calc(33.3333% / 2 - 24px); + width: 66%; + margin-right: auto; + margin-left: auto; margin-top: 10px; margin-bottom: 10px; padding: 0px; @@ -46,6 +46,12 @@ adf-start-task { .adf { + &-new-task-footer { + .mat-button { + text-transform: uppercase !important; + } + } + &-start-task-input-container .mat-input-wrapper { padding-top: 8px; } @@ -104,3 +110,11 @@ } } } + +@media (max-width: 600px) { + .adf-new-task-layout-card { + width: 90%; + margin-left: auto; + margin-right: auto; + } +} diff --git a/lib/process-services/task-list/components/task-audit.directive.ts b/lib/process-services/task-list/components/task-audit.directive.ts index 58ebc074fe..70a55f9681 100644 --- a/lib/process-services/task-list/components/task-audit.directive.ts +++ b/lib/process-services/task-list/components/task-audit.directive.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +/* tslint:disable:no-input-rename */ + import { ContentService } from '@alfresco/adf-core'; import { Directive, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { TaskListService } from './../services/tasklist.service'; diff --git a/lib/process-services/task-list/components/task-details.component.ts b/lib/process-services/task-list/components/task-details.component.ts index 414838adfa..17db8dce4e 100644 --- a/lib/process-services/task-list/components/task-details.component.ts +++ b/lib/process-services/task-list/components/task-details.component.ts @@ -16,7 +16,7 @@ */ import { PeopleProcessService, UserProcessModel } from '@alfresco/adf-core'; -import { AuthenticationService, CardViewUpdateService, ClickNotification, LogService, UpdateNotification } from '@alfresco/adf-core'; +import { AuthenticationService, CardViewUpdateService, ClickNotification, LogService, UpdateNotification, FormRenderingService } from '@alfresco/adf-core'; import { Component, EventEmitter, Input, @@ -35,6 +35,7 @@ import { TaskQueryRequestRepresentationModel } from '../models/filter.model'; import { TaskDetailsModel } from '../models/task-details.model'; import { TaskListService } from './../services/tasklist.service'; import { CommentsComponent } from '../../comments'; +import { AttachFileWidgetComponent, AttachFolderWidgetComponent } from '../../content-widget'; @Component({ selector: 'adf-task-details', @@ -141,9 +142,13 @@ export class TaskDetailsComponent implements OnInit, OnChanges { constructor(private taskListService: TaskListService, private authService: AuthenticationService, private peopleProcessService: PeopleProcessService, + private formRenderingService: FormRenderingService, private logService: LogService, private cardViewUpdateService: CardViewUpdateService, private dialog: MatDialog) { + + this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true); + this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true); this.peopleSearch$ = new Observable<UserProcessModel[]>(observer => this.peopleSearchObserver = observer).share(); } diff --git a/lib/process-services/task-list/components/task-header.component.spec.ts b/lib/process-services/task-list/components/task-header.component.spec.ts index e78ecabf0d..dacbfb492f 100644 --- a/lib/process-services/task-list/components/task-header.component.spec.ts +++ b/lib/process-services/task-list/components/task-header.component.spec.ts @@ -107,6 +107,14 @@ describe('TaskHeaderComponent', () => { expect(formNameEl.nativeElement.innerText).toBe('Wilbur Adams'); }); + it('should display priority', () => { + component.taskDetails.priority = 27; + component.ngOnChanges({}); + fixture.detectChanges(); + let formNameEl = fixture.debugElement.query(By.css('[data-automation-id="card-textitem-value-priority"]')); + expect(formNameEl.nativeElement.innerText).toBe('27'); + }); + it('should set editable to false if the task has already completed', () => { component.taskDetails.endDate = new Date('05/05/2002'); component.ngOnChanges({}); diff --git a/lib/process-services/task-list/components/task-header.component.ts b/lib/process-services/task-list/components/task-header.component.ts index 049f1655b6..0efe281723 100644 --- a/lib/process-services/task-list/components/task-header.component.ts +++ b/lib/process-services/task-list/components/task-header.component.ts @@ -17,7 +17,7 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { BpmUserService, CardViewDateItemModel, CardViewItem, CardViewMapItemModel, CardViewTextItemModel, LogService } from '@alfresco/adf-core'; +import { BpmUserService, CardViewDateItemModel, CardViewItem, CardViewMapItemModel, CardViewTextItemModel, LogService, TranslationService } from '@alfresco/adf-core'; import { TaskDetailsModel } from '../models/task-details.model'; import { TaskListService } from './../services/tasklist.service'; @@ -48,6 +48,7 @@ export class TaskHeaderComponent implements OnChanges, OnInit { constructor(private activitiTaskService: TaskListService, private bpmUserService: BpmUserService, + private translationService: TranslationService, private logService: LogService) { } @@ -71,7 +72,7 @@ export class TaskHeaderComponent implements OnChanges, OnInit { label: 'ADF_TASK_LIST.PROPERTIES.ASSIGNEE', value: this.taskDetails.getFullName(), key: 'assignee', - default: 'ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT', + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT'), clickable: !this.isCompleted() } ), @@ -86,7 +87,8 @@ export class TaskHeaderComponent implements OnChanges, OnInit { { label: 'ADF_TASK_LIST.PROPERTIES.PRIORITY', value: this.taskDetails.priority, - key: 'priority' + key: 'priority', + editable: true } ), new CardViewDateItemModel( @@ -94,7 +96,7 @@ export class TaskHeaderComponent implements OnChanges, OnInit { label: 'ADF_TASK_LIST.PROPERTIES.DUE_DATE', value: this.taskDetails.dueDate, key: 'dueDate', - default: 'ADF_TASK_LIST.PROPERTIES.DUE_DATE_DEFAULT', + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.DUE_DATE_DEFAULT'), editable: true } ), @@ -103,14 +105,14 @@ export class TaskHeaderComponent implements OnChanges, OnInit { label: 'ADF_TASK_LIST.PROPERTIES.CATEGORY', value: this.taskDetails.category, key: 'category', - default: 'ADF_TASK_LIST.PROPERTIES.CATEGORY_DEFAULT' + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.CATEGORY_DEFAULT') } ), new CardViewMapItemModel( { label: 'ADF_TASK_LIST.PROPERTIES.PARENT_NAME', value: parentInfoMap, key: 'parentName', - default: 'ADF_TASK_LIST.PROPERTIES.PARENT_NAME_DEFAULT', + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.PARENT_NAME_DEFAULT'), clickable: true } ), @@ -140,7 +142,7 @@ export class TaskHeaderComponent implements OnChanges, OnInit { label: 'ADF_TASK_LIST.PROPERTIES.DESCRIPTION', value: this.taskDetails.description, key: 'description', - default: 'ADF_TASK_LIST.PROPERTIES.DESCRIPTION_DEFAULT', + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.DESCRIPTION_DEFAULT'), multiline: true, editable: true } @@ -150,7 +152,7 @@ export class TaskHeaderComponent implements OnChanges, OnInit { label: 'ADF_TASK_LIST.PROPERTIES.FORM_NAME', value: this.formName, key: 'formName', - default: 'ADF_TASK_LIST.PROPERTIES.FORM_NAME_DEFAULT' + default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.FORM_NAME_DEFAULT') } ) ]; diff --git a/lib/process-services/task-list/components/task-list.component.css b/lib/process-services/task-list/components/task-list.component.css index a83c1106da..41e447251f 100644 --- a/lib/process-services/task-list/components/task-list.component.css +++ b/lib/process-services/task-list/components/task-list.component.css @@ -1,13 +1,13 @@ -alfresco-datatable >>> .column-header { +alfresco-datatable ::ng-deep .column-header { color: #232323; font-size: 15px; } -alfresco-datatable >>> .data-cell { +alfresco-datatable ::ng-deep .data-cell { cursor: pointer !important; } -alfresco-datatable >>> .cell-value{ +alfresco-datatable ::ng-deep .cell-value{ width: 250px; white-space: nowrap; overflow: hidden; diff --git a/lib/process-services/task-list/components/task-list.component.spec.ts b/lib/process-services/task-list/components/task-list.component.spec.ts index 9efeedb6c0..f74c495752 100644 --- a/lib/process-services/task-list/components/task-list.component.spec.ts +++ b/lib/process-services/task-list/components/task-list.component.spec.ts @@ -206,10 +206,10 @@ describe('TaskListComponent', () => { }); it('should fetch custom schemaColumn when the input presetColumn is defined', () => { - component.presetColumn = 'fakeCutomColumns'; + component.presetColumn = 'fakeCutomSchema'; fixture.detectChanges(); expect(component.data.getColumns()).toBeDefined(); - expect(component.data.getColumns().length).toEqual(3); + expect(component.data.getColumns().length).toEqual(2); }); it('should return an empty task list when no input parameters are passed', () => { diff --git a/lib/process-services/task-list/components/task-list.component.ts b/lib/process-services/task-list/components/task-list.component.ts index a3affa5a95..80a4b297fa 100644 --- a/lib/process-services/task-list/components/task-list.component.ts +++ b/lib/process-services/task-list/components/task-list.component.ts @@ -36,48 +36,74 @@ export class TaskListComponent implements OnChanges, OnInit, AfterContentInit { @ContentChild(DataColumnListComponent) columnList: DataColumnListComponent; + /* The id of the app. */ @Input() appId: number; + /* The Instance Id of the process. */ @Input() processInstanceId: string; + /* The Definition Key of the process. */ @Input() processDefinitionKey: string; + /* Current state of the process. Possible values are: `completed`, `active`. */ @Input() state: string; + /* The assignment of the process. Possible values are: "assignee" (the current user + * is the assignee), candidate (the current user is a task candidate", "group_x" (the task + * is assigned to a group where the current user is a member, + * no value(the current user is involved). + */ @Input() assignment: string; + /* Define the sort order of the processes. Possible values are : `created-desc`, + * `created-asc`, `due-desc`, `due-asc` + */ @Input() sort: string; @Input() name: string; + /* Define which task id should be selected after reloading. If the task id doesn't + * exist or nothing is passed then the first task will be selected. + */ @Input() landingTaskId: string; + /* Data source object that represents the number and the type of the columns that + * you want to show. + */ @Input() data: DataTableAdapter; + /* Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, + * you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for + * multiple rows. + */ @Input() selectionMode: string = 'single'; // none|single|multiple @Input() presetColumn: string; + /* Toggles multiple row selection, renders checkboxes at the beginning of each row */ @Input() multiselect: boolean = false; + /* Emitted when a task in the list is clicked */ @Output() rowClick: EventEmitter<string> = new EventEmitter<string>(); + /* Emitted when rows are selected/unselected */ @Output() rowsSelected: EventEmitter<any[]> = new EventEmitter<any[]>(); + /* Emitted when the task list is loaded */ @Output() success: EventEmitter<any> = new EventEmitter<any>(); @@ -88,9 +114,11 @@ export class TaskListComponent implements OnChanges, OnInit, AfterContentInit { selectedInstances: any[]; layoutPresets = {}; + /* The page number of the tasks to fetch. */ @Input() page: number = 0; + /* The number of tasks to fetch. */ @Input() size: number = DEFAULT_SIZE; @@ -145,21 +173,11 @@ export class TaskListComponent implements OnChanges, OnInit, AfterContentInit { * If component is assigned with an empty data adater the default schema settings applied. */ setupSchema(): void { - let schema: DataColumn[] = []; - - if (this.columnList && this.columnList.columns && this.columnList.columns.length > 0) { - schema = this.columnList.columns.map(c => <DataColumn> c); - } - + let schema = this.getSchema(); if (!this.data) { - this.data = new ObjectDataTableAdapter([], schema.length > 0 ? schema : this.presetColumn ? this.getLayoutPreset(this.presetColumn) : this.getLayoutPreset()); - - } else { - if (schema && schema.length > 0) { - this.data.setColumns(schema); - } else if (this.data.getColumns().length === 0) { - this.presetColumn ? this.setupDefaultColumns(this.presetColumn) : this.setupDefaultColumns(); - } + this.data = new ObjectDataTableAdapter([], schema); + } else if (this.data.getColumns().length === 0) { + this.data.setColumns(schema); } } @@ -341,13 +359,6 @@ export class TaskListComponent implements OnChanges, OnInit, AfterContentInit { return new TaskQueryRequestRepresentationModel(requestNode); } - setupDefaultColumns(preset: string = 'default'): void { - if (this.data) { - const columns = this.getLayoutPreset(preset); - this.data.setColumns(columns); - } - } - private loadLayoutPresets(): void { const externalSettings = this.appConfig.get('adf-task-list.presets', null); @@ -356,10 +367,30 @@ export class TaskListComponent implements OnChanges, OnInit, AfterContentInit { } else { this.layoutPresets = taskPresetsDefaultModel; } - } - private getLayoutPreset(name: string = 'default'): DataColumn[] { - return (this.layoutPresets[name] || this.layoutPresets['default']).map(col => new ObjectDataColumn(col)); + getSchema(): any { + let customSchemaColumns = []; + customSchemaColumns = this.getSchemaFromConfig(this.presetColumn).concat(this.getSchemaFromHtml()); + if (customSchemaColumns.length === 0) { + customSchemaColumns = this.getDefaultLayoutPreset(); + } + return customSchemaColumns; + } + + getSchemaFromHtml(): any { + let schema = []; + if (this.columnList && this.columnList.columns && this.columnList.columns.length > 0) { + schema = this.columnList.columns.map(c => <DataColumn> c); + } + return schema; + } + + private getSchemaFromConfig(name: string): DataColumn[] { + return name ? (this.layoutPresets[name]).map(col => new ObjectDataColumn(col)) : []; + } + + private getDefaultLayoutPreset(): DataColumn[] { + return (this.layoutPresets['default']).map(col => new ObjectDataColumn(col)); } } diff --git a/lib/process-services/task-list/public-api.ts b/lib/process-services/task-list/public-api.ts index c9a291d354..b25ea0ad48 100644 --- a/lib/process-services/task-list/public-api.ts +++ b/lib/process-services/task-list/public-api.ts @@ -30,8 +30,10 @@ export * from './services/task-upload.service'; export * from './services/task-filter.service'; export * from './models/filter.model'; -export * from './models/task-details.model'; -export * from './models/task-details.event'; -export * from './models/user-event.model'; +export * from './models/form.model'; export * from './models/start-task.model'; +export * from './models/task-details.event'; +export * from './models/task-details.model'; export * from './models/task-list.model'; +export * from './models/user-event.model'; +export * from './models/user-group.model'; diff --git a/lib/process-services/task-list/services/task-filter.service.spec.ts b/lib/process-services/task-list/services/task-filter.service.spec.ts index 996db0e67e..df7555ef60 100644 --- a/lib/process-services/task-list/services/task-filter.service.spec.ts +++ b/lib/process-services/task-list/services/task-filter.service.spec.ts @@ -26,27 +26,21 @@ import { TaskFilterService } from './task-filter.service'; declare let jasmine: any; -describe('Activiti Tas filter Service', () => { +describe('Activiti Task filter Service', () => { let service: TaskFilterService; beforeEach(async(() => { TestBed.configureTestingModule({ - providers: [ TaskFilterService ] - }).compileComponents(); + }).compileComponents().then(() => { + service = TestBed.get(TaskFilterService); + jasmine.Ajax.install(); + }); })); - beforeEach(() => { - service = TestBed.get(TaskFilterService); - }); - - beforeEach(() => { - jasmine.Ajax.install(); - }); - afterEach(() => { jasmine.Ajax.uninstall(); }); @@ -54,15 +48,13 @@ describe('Activiti Tas filter Service', () => { describe('Content tests', () => { it('should return the task list filters', (done) => { - service.getTaskListFilters().subscribe( - (res) => { - expect(res).toBeDefined(); - expect(res.length).toEqual(2); - expect(res[0].name).toEqual('FakeInvolvedTasks'); - expect(res[1].name).toEqual('FakeMyTasks'); - done(); - } - ); + service.getTaskListFilters().subscribe((res) => { + expect(res).toBeDefined(); + expect(res.length).toEqual(2); + expect(res[0].name).toEqual('FakeInvolvedTasks'); + expect(res[1].name).toEqual('FakeMyTasks'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -72,17 +64,15 @@ describe('Activiti Tas filter Service', () => { }); it('should return the task filter by id', (done) => { - service.getTaskFilterById(2).subscribe( - (taskFilter: FilterRepresentationModel) => { - expect(taskFilter).toBeDefined(); - expect(taskFilter.id).toEqual(2); - expect(taskFilter.name).toEqual('FakeMyTasks'); - expect(taskFilter.filter.sort).toEqual('created-desc'); - expect(taskFilter.filter.state).toEqual('open'); - expect(taskFilter.filter.assignment).toEqual('fake-assignee'); - done(); - } - ); + service.getTaskFilterById(2).subscribe((taskFilter: FilterRepresentationModel) => { + expect(taskFilter).toBeDefined(); + expect(taskFilter.id).toEqual(2); + expect(taskFilter.name).toEqual('FakeMyTasks'); + expect(taskFilter.filter.sort).toEqual('created-desc'); + expect(taskFilter.filter.state).toEqual('open'); + expect(taskFilter.filter.assignment).toEqual('fake-assignee'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -92,8 +82,7 @@ describe('Activiti Tas filter Service', () => { }); it('should return the task filter by name', (done) => { - service.getTaskFilterByName('FakeMyTasks').subscribe( - (res: FilterRepresentationModel) => { + service.getTaskFilterByName('FakeMyTasks').subscribe((res: FilterRepresentationModel) => { expect(res).toBeDefined(); expect(res.id).toEqual(2); expect(res.name).toEqual('FakeMyTasks'); @@ -111,28 +100,24 @@ describe('Activiti Tas filter Service', () => { }); }); - it('should call the api withthe appId', (done) => { + it('should call the api with the appId', (done) => { spyOn(service, 'callApiTaskFilters').and.returnValue((fakeAppPromise)); let appId = 1; - service.getTaskListFilters(appId).subscribe( - (res) => { - expect(service.callApiTaskFilters).toHaveBeenCalledWith(appId); - done(); - } - ); + service.getTaskListFilters(appId).subscribe((res) => { + expect(service.callApiTaskFilters).toHaveBeenCalledWith(appId); + done(); + }); }); it('should return the app filter by id', (done) => { let appId = 1; - service.getTaskListFilters(appId).subscribe( - (res) => { - expect(res).toBeDefined(); - expect(res.length).toEqual(1); - expect(res[0].name).toEqual('FakeInvolvedTasks'); - done(); - } - ); + service.getTaskListFilters(appId).subscribe((res) => { + expect(res).toBeDefined(); + expect(res.length).toEqual(1); + expect(res[0].name).toEqual('FakeInvolvedTasks'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -142,23 +127,25 @@ describe('Activiti Tas filter Service', () => { }); it('should return the default filters', (done) => { - service.createDefaultFilters(1234).subscribe( - (res: FilterRepresentationModel []) => { - expect(res).toBeDefined(); - expect(res.length).toEqual(4); - expect(res[0].name).toEqual('Involved Tasks'); - expect(res[1].name).toEqual('My Tasks'); - expect(res[2].name).toEqual('Queued Tasks'); - expect(res[3].name).toEqual('Completed Tasks'); - done(); - } - ); + service.createDefaultFilters(1234).subscribe((res: FilterRepresentationModel []) => { + expect(res).toBeDefined(); + expect(res.length).toEqual(4); + expect(res[0].name).toEqual('Involved Tasks'); + expect(res[0].id).toEqual(111); + expect(res[1].name).toEqual('My Tasks'); + expect(res[1].id).toEqual(222); + expect(res[2].name).toEqual('Queued Tasks'); + expect(res[2].id).toEqual(333); + expect(res[3].name).toEqual('Completed Tasks'); + expect(res[3].id).toEqual(444); + done(); + }); jasmine.Ajax.requests.at(0).respondWith({ 'status': 200, contentType: 'application/json', responseText: JSON.stringify({ - id: '111', name: 'Involved Tasks', filter: { assignment: 'fake-involved' } + appId: 1001, id: '111', name: 'Involved Tasks', icon: 'fake-icon', recent: false }) }); @@ -166,7 +153,7 @@ describe('Activiti Tas filter Service', () => { 'status': 200, contentType: 'application/json', responseText: JSON.stringify({ - id: '222', name: 'My Tasks', filter: { assignment: 'fake-assignee' } + appId: 1001, id: '222', name: 'My Tasks', icon: 'fake-icon', recent: false }) }); @@ -174,7 +161,7 @@ describe('Activiti Tas filter Service', () => { 'status': 200, contentType: 'application/json', responseText: JSON.stringify({ - id: '333', name: 'Queued Tasks', filter: { assignment: 'fake-candidate' } + appId: 1001, id: '333', name: 'Queued Tasks', icon: 'fake-icon', recent: false }) }); @@ -182,26 +169,24 @@ describe('Activiti Tas filter Service', () => { 'status': 200, contentType: 'application/json', responseText: JSON.stringify({ - id: '444', name: 'Completed Tasks', filter: { assignment: 'fake-involved' } + appId: 1001, id: '444', name: 'Completed Tasks', icon: 'fake-icon', recent: false }) }); }); - it('should add a filter ', (done) => { + it('should add a filter', (done) => { let filterFake = new FilterRepresentationModel({ name: 'FakeNameFilter', assignment: 'fake-assignement' }); - service.addFilter(filterFake).subscribe( - (res: FilterRepresentationModel) => { - expect(res).toBeDefined(); - expect(res.id).not.toEqual(null); - expect(res.name).toEqual('FakeNameFilter'); - expect(res.filter.assignment).toEqual('fake-assignement'); - done(); - } - ); + service.addFilter(filterFake).subscribe((res: FilterRepresentationModel) => { + expect(res).toBeDefined(); + expect(res.id).not.toEqual(null); + expect(res.name).toEqual('FakeNameFilter'); + expect(res.filter.assignment).toEqual('fake-assignement'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, diff --git a/lib/process-services/task-list/services/task-filter.service.ts b/lib/process-services/task-list/services/task-filter.service.ts index 7e7beaf95b..dc892c25f9 100644 --- a/lib/process-services/task-list/services/task-filter.service.ts +++ b/lib/process-services/task-list/services/task-filter.service.ts @@ -63,12 +63,16 @@ export class TaskFilterService { let filters: FilterRepresentationModel[] = []; res.forEach((filter) => { if (filter.name === involvedTasksFilter.name) { + involvedTasksFilter.id = filter.id; filters.push(involvedTasksFilter); } else if (filter.name === myTasksFilter.name) { + myTasksFilter.id = filter.id; filters.push(myTasksFilter); } else if (filter.name === queuedTasksFilter.name) { + queuedTasksFilter.id = filter.id; filters.push(queuedTasksFilter); } else if (filter.name === completedTasksFilter.name) { + completedTasksFilter.id = filter.id; filters.push(completedTasksFilter); } }); diff --git a/lib/process-services/task-list/services/tasklist.service.spec.ts b/lib/process-services/task-list/services/tasklist.service.spec.ts index d3574a376c..b7f3b1c5ec 100644 --- a/lib/process-services/task-list/services/tasklist.service.spec.ts +++ b/lib/process-services/task-list/services/tasklist.service.spec.ts @@ -40,27 +40,21 @@ import { TaskListService } from './tasklist.service'; declare let jasmine: any; -xdescribe('Activiti TaskList Service', () => { +describe('Activiti TaskList Service', () => { let service: TaskListService; beforeEach(async(() => { TestBed.configureTestingModule({ - providers: [ TaskListService ] - }).compileComponents(); + }).compileComponents().then(() => { + service = TestBed.get(TaskListService); + jasmine.Ajax.install(); + }); })); - beforeEach(() => { - service = TestBed.get(TaskListService); - }); - - beforeEach(() => { - jasmine.Ajax.install(); - }); - afterEach(() => { jasmine.Ajax.uninstall(); }); @@ -68,20 +62,18 @@ xdescribe('Activiti TaskList Service', () => { describe('Content tests', () => { it('should return the task list filtered', (done) => { - service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(1); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(1); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.size).toEqual(1); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(1); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -91,20 +83,18 @@ xdescribe('Activiti TaskList Service', () => { }); it('should return the task list filtered by processDefinitionKey', (done) => { - service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(2); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(2); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.size).toEqual(2); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(2); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -113,14 +103,13 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should throw an exception when the response is wrong', () => { - service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - (res) => { + it('should throw an exception when the response is wrong', (done) => { + service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => { }, (err: any) => { expect(err).toBeDefined(); - } - ); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 404, @@ -133,58 +122,52 @@ xdescribe('Activiti TaskList Service', () => { spyOn(service, 'getTasks').and.returnValue(Observable.of(fakeTaskList)); spyOn(service, 'getTotalTasks').and.returnValue(Observable.of(fakeTaskList)); - service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe( - res => { + service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe(res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(1); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(1); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + expect(res).toBeDefined(); + expect(res.size).toEqual(1); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(1); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); }); it('should return the task list with all tasks filtered', (done) => { spyOn(service, 'getTasks').and.returnValue(Observable.of(fakeTaskList)); spyOn(service, 'getTotalTasks').and.returnValue(Observable.of(fakeTaskList)); - service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(1); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(1); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.size).toEqual(1); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(1); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); }); it('should return the task list filtered by state', (done) => { - service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe( - res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(1); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(1); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe(res => { + expect(res).toBeDefined(); + expect(res.size).toEqual(1); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(1); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -194,19 +177,17 @@ xdescribe('Activiti TaskList Service', () => { }); it('should return the task list filtered', (done) => { - service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res.size).toEqual(1); - expect(res.start).toEqual(0); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(1); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res.size).toEqual(1); + expect(res.start).toEqual(0); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(1); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -219,83 +200,67 @@ xdescribe('Activiti TaskList Service', () => { spyOn(service, 'getTasks').and.returnValue(Observable.of(fakeTaskList)); spyOn(service, 'getTotalTasks').and.returnValue(Observable.of(fakeTaskList)); - service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(2); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[0].assignee.firstName).toEqual('firstName'); - expect(res.data[0].assignee.lastName).toEqual('lastName'); + service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(2); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[0].assignee.firstName).toEqual('firstName'); + expect(res.data[0].assignee.lastName).toEqual('lastName'); - expect(res.data[1].name).toEqual('FakeNameTask'); - expect(res.data[1].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[1].assignee.firstName).toEqual('firstName'); - expect(res.data[1].assignee.lastName).toEqual('lastName'); - done(); - } - ); + expect(res.data[1].name).toEqual('FakeNameTask'); + expect(res.data[1].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[1].assignee.firstName).toEqual('firstName'); + expect(res.data[1].assignee.lastName).toEqual('lastName'); + done(); + }); }); it('Should return both open and completed task', (done) => { spyOn(service, 'findTasksByState').and.returnValue(Observable.of(fakeOpenTaskList)); spyOn(service, 'findAllTaskByState').and.returnValue(Observable.of(fakeCompletedTaskList)); - service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(4); - expect(res.data[0].name).toEqual('FakeOpenTask1'); - expect(res.data[1].assignee.email).toEqual('fake-open-email@dom.com'); - expect(res.data[2].name).toEqual('FakeCompletedTaskName1'); - expect(res.data[2].assignee.email).toEqual('fake-completed-email@dom.com'); - expect(res.data[3].name).toEqual('FakeCompletedTaskName2'); - done(); - } - ); + service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(4); + expect(res.data[0].name).toEqual('FakeOpenTask1'); + expect(res.data[1].assignee.email).toEqual('fake-open-email@dom.com'); + expect(res.data[2].name).toEqual('FakeCompletedTaskName1'); + expect(res.data[2].assignee.email).toEqual('fake-completed-email@dom.com'); + expect(res.data[3].name).toEqual('FakeCompletedTaskName2'); + done(); + }); }); it('should add the task list to the tasklistSubject with all tasks filtered without state', (done) => { spyOn(service, 'getTasks').and.returnValue(Observable.of(fakeTaskList)); spyOn(service, 'getTotalTasks').and.returnValue(Observable.of(fakeTaskList)); - service.tasksList$.subscribe( - (res) => { - expect(res).toBeDefined(); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(2); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[1].assignee.email).toEqual('fake-email@dom.com'); - }); - service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.data).toBeDefined(); - expect(res.data.length).toEqual(2); - expect(res.data[0].name).toEqual('FakeNameTask'); - expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res.data[1].name).toEqual('FakeNameTask'); - expect(res.data[1].assignee.email).toEqual('fake-email@dom.com'); - done(); - } - ); + service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe(res => { + expect(res).toBeDefined(); + expect(res.data).toBeDefined(); + expect(res.data.length).toEqual(2); + expect(res.data[0].name).toEqual('FakeNameTask'); + expect(res.data[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res.data[1].name).toEqual('FakeNameTask'); + expect(res.data[1].assignee.email).toEqual('fake-email@dom.com'); + done(); + }); }); - it('should return the task details ', (done) => { - service.getTaskDetails('999').subscribe( - (res: TaskDetailsModel) => { - expect(res).toBeDefined(); - expect(res.id).toEqual('999'); - expect(res.name).toEqual('fake-task-name'); - expect(res.formKey).toEqual('99'); - expect(res.assignee).toBeDefined(); - expect(res.assignee.email).toEqual('fake-email@dom.com'); - expect(res.assignee.firstName).toEqual('firstName'); - expect(res.assignee.lastName).toEqual('lastName'); - done(); - } - ); + xit('should return the task details ', (done) => { + service.getTaskDetails('999').subscribe((res: TaskDetailsModel) => { + expect(res).toBeDefined(); + expect(res.id).toEqual('999'); + expect(res.name).toEqual('fake-task-name'); + expect(res.formKey).toEqual('99'); + expect(res.assignee).toBeDefined(); + expect(res.assignee.email).toEqual('fake-email@dom.com'); + expect(res.assignee.firstName).toEqual('firstName'); + expect(res.assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -304,22 +269,20 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should return the tasks checklists ', (done) => { - service.getTaskChecklist('999').subscribe( - (res: TaskDetailsModel[]) => { - expect(res).toBeDefined(); - expect(res.length).toEqual(2); - expect(res[0].name).toEqual('FakeCheckTask1'); - expect(res[0].assignee.email).toEqual('fake-email@dom.com'); - expect(res[0].assignee.firstName).toEqual('firstName'); - expect(res[0].assignee.lastName).toEqual('lastName'); - expect(res[1].name).toEqual('FakeCheckTask2'); - expect(res[1].assignee.email).toEqual('fake-email@dom.com'); - expect(res[1].assignee.firstName).toEqual('firstName'); - expect(res[0].assignee.lastName).toEqual('lastName'); - done(); - } - ); + xit('should return the tasks checklists ', (done) => { + service.getTaskChecklist('999').subscribe((res: TaskDetailsModel[]) => { + expect(res).toBeDefined(); + expect(res.length).toEqual(2); + expect(res[0].name).toEqual('FakeCheckTask1'); + expect(res[0].assignee.email).toEqual('fake-email@dom.com'); + expect(res[0].assignee.firstName).toEqual('firstName'); + expect(res[0].assignee.lastName).toEqual('lastName'); + expect(res[1].name).toEqual('FakeCheckTask2'); + expect(res[1].assignee.email).toEqual('fake-email@dom.com'); + expect(res[1].assignee.firstName).toEqual('firstName'); + expect(res[0].assignee.lastName).toEqual('lastName'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -328,7 +291,7 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should add a task ', (done) => { + xit('should add a task ', (done) => { let taskFake = new TaskDetailsModel({ id: 123, parentTaskId: 456, @@ -339,15 +302,13 @@ xdescribe('Activiti TaskList Service', () => { created: '' }); - service.addTask(taskFake).subscribe( - (res: TaskDetailsModel) => { - expect(res).toBeDefined(); - expect(res.id).not.toEqual(''); - expect(res.name).toEqual('FakeNameTask'); - expect(res.created).not.toEqual(null); - done(); - } - ); + service.addTask(taskFake).subscribe((res: TaskDetailsModel) => { + expect(res).toBeDefined(); + expect(res.id).not.toEqual(''); + expect(res.name).toEqual('FakeNameTask'); + expect(res.created).not.toEqual(null); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -360,12 +321,10 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should remove a checklist task ', (done) => { - service.deleteTask('999').subscribe( - () => { - done(); - } - ); + xit('should remove a checklist task ', (done) => { + service.deleteTask('999').subscribe(() => { + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -373,13 +332,11 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should complete the task ', (done) => { - service.completeTask('999').subscribe( - (res: any) => { - expect(res).toBeDefined(); - done(); - } - ); + xit('should complete the task', (done) => { + service.completeTask('999').subscribe((res: any) => { + expect(res).toBeDefined(); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -388,15 +345,13 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should return the total number of tasks', (done) => { - service.getTotalTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe( - res => { - expect(res).toBeDefined(); - expect(res.size).toEqual(1); - expect(res.total).toEqual(1); - done(); - } - ); + xit('should return the total number of tasks', (done) => { + service.getTotalTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res: any) => { + expect(res).toBeDefined(); + expect(res.size).toEqual(1); + expect(res.total).toEqual(1); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -405,24 +360,22 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should create a new standalone task ', (done) => { + xit('should create a new standalone task ', (done) => { let taskFake = new TaskDetailsModel({ name: 'FakeNameTask', description: 'FakeDescription', category: '3' }); - service.createNewTask(taskFake).subscribe( - (res: TaskDetailsModel) => { - expect(res).toBeDefined(); - expect(res.id).not.toEqual(''); - expect(res.name).toEqual('FakeNameTask'); - expect(res.description).toEqual('FakeDescription'); - expect(res.category).toEqual('3'); - expect(res.created).not.toEqual(null); - done(); - } - ); + service.createNewTask(taskFake).subscribe((res: TaskDetailsModel) => { + expect(res).toBeDefined(); + expect(res.id).not.toEqual(''); + expect(res.name).toEqual('FakeNameTask'); + expect(res.description).toEqual('FakeDescription'); + expect(res.category).toEqual('3'); + expect(res.created).not.toEqual(null); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -438,25 +391,23 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should assign task to a user', (done) => { + xit('should assign task to a user', (done) => { let testTaskId = '8888'; - service.assignTask(testTaskId, fakeUser2).subscribe( - (res: TaskDetailsModel) => { - expect(res).toBeDefined(); - expect(res.id).toEqual(testTaskId); - expect(res.name).toEqual('FakeNameTask'); - expect(res.description).toEqual('FakeDescription'); - expect(res.category).toEqual('3'); - expect(res.created).not.toEqual(null); - expect(res.adhocTaskCanBeReassigned).toBe(true); - expect(res.assignee).toEqual(new UserProcessModel(fakeUser2)); - expect(res.involvedPeople[0].email).toEqual(fakeUser1.email); - expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName); - expect(res.involvedPeople[0].lastName).toEqual(fakeUser1.lastName); - expect(res.involvedPeople[0].id).toEqual(fakeUser1.id); - done(); - } - ); + service.assignTask(testTaskId, fakeUser2).subscribe((res: TaskDetailsModel) => { + expect(res).toBeDefined(); + expect(res.id).toEqual(testTaskId); + expect(res.name).toEqual('FakeNameTask'); + expect(res.description).toEqual('FakeDescription'); + expect(res.category).toEqual('3'); + expect(res.created).not.toEqual(null); + expect(res.adhocTaskCanBeReassigned).toBe(true); + expect(res.assignee).toEqual(new UserProcessModel(fakeUser2)); + expect(res.involvedPeople[0].email).toEqual(fakeUser1.email); + expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName); + expect(res.involvedPeople[0].lastName).toEqual(fakeUser1.lastName); + expect(res.involvedPeople[0].id).toEqual(fakeUser1.id); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -474,23 +425,21 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should assign task to a userId', (done) => { + xit('should assign task to a userId', (done) => { let testTaskId = '8888'; - service.assignTaskByUserId(testTaskId, fakeUser2.id).subscribe( - (res: TaskDetailsModel) => { - expect(res).toBeDefined(); - expect(res.id).toEqual(testTaskId); - expect(res.name).toEqual('FakeNameTask'); - expect(res.description).toEqual('FakeDescription'); - expect(res.category).toEqual('3'); - expect(res.created).not.toEqual(null); - expect(res.adhocTaskCanBeReassigned).toBe(true); - expect(res.assignee).toEqual(new UserProcessModel(fakeUser2)); - expect(res.involvedPeople[0].email).toEqual(fakeUser1.email); - expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName); - done(); - } - ); + service.assignTaskByUserId(testTaskId, fakeUser2.id).subscribe((res: TaskDetailsModel) => { + expect(res).toBeDefined(); + expect(res.id).toEqual(testTaskId); + expect(res.name).toEqual('FakeNameTask'); + expect(res.description).toEqual('FakeDescription'); + expect(res.category).toEqual('3'); + expect(res.created).not.toEqual(null); + expect(res.adhocTaskCanBeReassigned).toBe(true); + expect(res.assignee).toEqual(new UserProcessModel(fakeUser2)); + expect(res.involvedPeople[0].email).toEqual(fakeUser1.email); + expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -508,14 +457,12 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should claim a task', (done) => { + xit('should claim a task', (done) => { let taskId = '111'; - service.claimTask(taskId).subscribe( - (res: any) => { - done(); - } - ); + service.claimTask(taskId).subscribe(() => { + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -524,14 +471,12 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should unclaim a task', (done) => { + xit('should unclaim a task', (done) => { let taskId = '111'; - service.unclaimTask(taskId).subscribe( - (res: any) => { - done(); - } - ); + service.unclaimTask(taskId).subscribe(() => { + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -540,14 +485,12 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should update a task', (done) => { + xit('should update a task', (done) => { let taskId = '111'; - service.updateTask(taskId, {property: 'value'}).subscribe( - (res: any) => { - done(); - } - ); + service.updateTask(taskId, { property: 'value' }).subscribe(() => { + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -556,7 +499,7 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should return the filter if it contains task id', (done) => { + xit('should return the filter if it contains task id', (done) => { let taskId = '1'; let filterFake = new FilterRepresentationModel({ name: 'FakeNameFilter', @@ -569,13 +512,11 @@ xdescribe('Activiti TaskList Service', () => { } }); - service.isTaskRelatedToFilter(taskId, filterFake).subscribe( - (res: any) => { - expect(res).toBeDefined(); - expect(res).not.toBeNull(); - done(); - } - ); + service.isTaskRelatedToFilter(taskId, filterFake).subscribe((res: any) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -584,23 +525,21 @@ xdescribe('Activiti TaskList Service', () => { }); }); - it('should return the filters if it contains task id', async(() => { + xit('should return the filters if it contains task id', (done) => { let taskId = '1'; let fakeFilterList: FilterRepresentationModel[] = []; fakeFilterList.push(fakeRepresentationFilter1, fakeRepresentationFilter2); let resultFilter: FilterRepresentationModel = null; - service.getFilterForTaskById(taskId, fakeFilterList).subscribe( - (res: FilterRepresentationModel) => { - resultFilter = res; - }, - () => { - }, - () => { - expect(resultFilter).toBeDefined(); - expect(resultFilter).not.toBeNull(); - expect(resultFilter.name).toBe('CONTAIN FILTER'); - }); + service.getFilterForTaskById(taskId, fakeFilterList).subscribe((res: FilterRepresentationModel) => { + resultFilter = res; + }, () => { + }, () => { + expect(resultFilter).toBeDefined(); + expect(resultFilter).not.toBeNull(); + expect(resultFilter.name).toBe('CONTAIN FILTER'); + done(); + }); jasmine.Ajax.requests.at(0).respondWith({ 'status': 200, @@ -613,20 +552,18 @@ xdescribe('Activiti TaskList Service', () => { contentType: 'application/json', responseText: JSON.stringify(secondFakeTaskList) }); - })); + }); - it('should get possibile form list', (done) => { - service.getFormList().subscribe( - (res: any) => { - expect(res).toBeDefined(); - expect(res.length).toBe(2); - expect(res[0].id).toBe(1); - expect(res[0].name).toBe('form with all widgets'); - expect(res[1].id).toBe(2); - expect(res[1].name).toBe('uppy'); - done(); - } - ); + xit('should get possible form list', (done) => { + service.getFormList().subscribe((res: any) => { + expect(res).toBeDefined(); + expect(res.length).toBe(2); + expect(res[0].id).toBe(1); + expect(res[0].name).toBe('form with all widgets'); + expect(res[1].id).toBe(2); + expect(res[1].name).toBe('uppy'); + done(); + }); jasmine.Ajax.requests.mostRecent().respondWith({ 'status': 200, @@ -635,5 +572,4 @@ xdescribe('Activiti TaskList Service', () => { }); }); }); - }); diff --git a/lib/process-services/task-list/task-list.module.ts b/lib/process-services/task-list/task-list.module.ts index 7ed4c1bec0..ab8010e789 100644 --- a/lib/process-services/task-list/task-list.module.ts +++ b/lib/process-services/task-list/task-list.module.ts @@ -27,6 +27,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MaterialModule } from '../material.module'; import { PeopleModule } from '../people/people.module'; import { CommentsModule } from '../comments/comments.module'; +import { ContentWidgetModule } from '../content-widget/content-widget.module'; import { TaskUploadService } from './services/task-upload.service'; import { ProcessUploadService } from './services/process-upload.service'; import { TaskListService } from './services/tasklist.service'; @@ -56,7 +57,8 @@ import { TaskListComponent } from './components/task-list.component'; FormsModule, ReactiveFormsModule, PeopleModule, - CommentsModule + CommentsModule, + ContentWidgetModule ], declarations: [ NoTaskDetailsTemplateDirective, diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 0530c87b5a..c375ff0340 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -27,6 +27,7 @@ "@alfresco/adf-insights": ["./analytics"], "alfresco-js-api": ["./node_modules/alfresco-js-api/"], "@angular/*": ["./node_modules/@angular/*"], + "@mat-datetimepicker/*": ["./node_modules/@mat-datetimepicker/*"], "rxjs/*": ["./node_modules/rxjs/*"] }, "lib": [ diff --git a/lib/tslint.json b/lib/tslint.json index 30901bcb6e..597f0ceb03 100644 --- a/lib/tslint.json +++ b/lib/tslint.json @@ -9,7 +9,10 @@ "parameters", "statements" ], - "ban": false, + "ban": [ + true, + "eval" + ], "class-name": true, "comment-format": [ true, @@ -152,6 +155,8 @@ "no-access-missing-member": false, "adf-file-name": true, "adf-class-name": true, - "adf-prefix-name": true + "adf-prefix-name": true, + "no-input-rename": true, + "no-output-rename": true } } diff --git a/scripts/import-langs.sh b/scripts/import-langs.sh index d0cfb374a9..064d5b9b6f 100755 --- a/scripts/import-langs.sh +++ b/scripts/import-langs.sh @@ -5,9 +5,9 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" LANG_ROOT="./i18n" show_help() { - echo "Usage: import-langs.sh import the i18n files from a folder" + echo "Usage: import-langs.sh --input NAME_FOLDER import the i18n files from a folder" echo "" - echo "--output or -o to specify a folder otherwise it will be created in i18n" + echo "--input or -o to specify a folder otherwise it will be created in i18n" } input_folder(){ diff --git a/scripts/next_version.sh b/scripts/next_version.sh index ca1f65c01e..3033a801f3 100755 --- a/scripts/next_version.sh +++ b/scripts/next_version.sh @@ -51,7 +51,7 @@ done if [[ "${EXEC_MINOR}" == true ]] then - ADF_VERSION=$(npm view ng2-alfresco-core version) + ADF_VERSION=$(npm view @alfresco/adf-core version) NEXT_VERSION=( ${ADF_VERSION//./ } ) ((NEXT_VERSION[1]++)) NEXT_VERSION[2]=0 @@ -60,7 +60,7 @@ fi if [[ "${EXEC_MAJOR}" == true ]] then - ADF_VERSION=$(npm view ng2-alfresco-core version) + ADF_VERSION=$(npm view @alfresco/adf-core version) NEXT_VERSION=( ${ADF_VERSION//./ } ) ((NEXT_VERSION[0]++)) NEXT_VERSION[1]=0 diff --git a/scripts/npm-build-all.sh b/scripts/npm-build-all.sh index 42c9fe1066..c9d2ffdbad 100755 --- a/scripts/npm-build-all.sh +++ b/scripts/npm-build-all.sh @@ -23,8 +23,8 @@ eval projects=( "core" show_help() { echo "Usage: npm-build-all.sh" echo "" - echo "-t or --test <pkg> Build your local components and run their tests, this parameter also accept a wildecard to execute test e.g: -t "ng2-alfresco-core"." - echo "-d or --debug <pkg> Build your local components and run their tests IN THE BROWSER, this parameter also accept a wildecard to execute test e.g: -t "ng2-alfresco-core"." + echo "-t or --test <pkg> Build your local components and run their tests, this parameter also accept a wildecard to execute test e.g: -t "core"." + echo "-d or --debug <pkg> Build your local components and run their tests IN THE BROWSER, this parameter also accept a wildecard to execute test e.g: -t "core"." echo "-c or --clean Clean the node_modules before starting the build." echo "-si or --skipinstall Skip the install of node_modules before starting the build." echo "-sb or --skipbuild Skip the build of ng-components." @@ -54,7 +54,7 @@ enable_testbrowser(){ test_project() { echo "====== test project: $1 =====" - npm run test -- --component $1 || exit 1 + npm run test -- --component $1 --mode coverage || exit 1 } debug_project() { diff --git a/scripts/npm-check-bundles.sh b/scripts/npm-check-bundles.sh index 6a7c9cf44e..e9d6e9a7a2 100755 --- a/scripts/npm-check-bundles.sh +++ b/scripts/npm-check-bundles.sh @@ -82,6 +82,20 @@ do echo "js.map ok!" fi + if [ ! -f package/esm5/$PACKAGE'.js' ]; then + error_out '31;1' "esm5/$PACKAGE.js not found!" >&2 + exit 1 + else + echo "esm5 ok!" + fi + + if [ ! -f package/esm2015/$PACKAGE'.js' ]; then + error_out '31;1' "esm2015/$PACKAGE.js not found!" >&2 + exit 1 + else + echo "esm2015 ok!" + fi + if [ ! -f package/_theming.scss ]; then error_out '31;1' "$PACKAGE style not found!" >&2 exit 1 diff --git a/scripts/update-version.sh b/scripts/update-version.sh index 520b7a3c5a..90ea74760f 100755 --- a/scripts/update-version.sh +++ b/scripts/update-version.sh @@ -23,6 +23,8 @@ show_help() { echo "-vj or -versionjsapi to use a different version of js-api" echo "-demoshell execute the change version only in the demo shell " echo "-v or -version version to update" + echo "-nextalpha update next alpha version of js-api and lib automatically" + echo "-nextbeta update next beta version of js-api and lib automatically" echo "-alpha update last alpha version of js-api and lib automatically" echo "-beta update beta alpha version of js-api and lib automatically" echo "-gnu for gnu" @@ -47,7 +49,7 @@ last_alpha_mode() { next_alpha_mode() { echo "====== Auto find next ALPHA version =====" - VERSION=$(./next_version.sh -major -alpha) + VERSION=$(./next_version.sh -minor -alpha) echo "====== version lib ${VERSION} =====" @@ -59,7 +61,7 @@ next_alpha_mode() { next_beta_mode() { echo "====== Auto find next BETA version =====" - VERSION=$(./next_version.sh -major -beta) + VERSION=$(./next_version.sh -minor -beta) echo "====== version lib ${VERSION} =====" @@ -71,7 +73,7 @@ next_beta_mode() { last_beta_mode() { echo "====== Auto find last BETA version =====" - VERSION=$(npm view ng2-alfresco-core@beta version) + VERSION=$(npm view @alfresco/adf-core@beta version) echo "====== version lib ${VERSION} =====" diff --git a/tslint.json b/tslint.json index ce80085e69..d9d1e66e41 100644 --- a/tslint.json +++ b/tslint.json @@ -1,17 +1,23 @@ { + "rulesDirectory": [ + "node_modules/codelyzer", + "node_modules/adf-tslint-rules" + ], "rules": { "align": [ true, "parameters", - "arguments", "statements" ], - "ban": false, + "ban": [ + true, + "eval", + {"name": ["fdescribe", "fit"], "message": "don't focus tests"} + ], "class-name": true, "comment-format": [ true, - "check-space", - "check-lowercase" + "check-space" ], "curly": true, "eofline": true, @@ -23,24 +29,23 @@ "interface-name": false, "jsdoc-format": true, "label-position": true, - "label-undefined": true, "max-line-length": [ true, - 140 + 180 ], "member-ordering": [ true, - "public-before-private", "static-before-instance", "variables-before-functions" ], "no-any": false, "no-arg": true, - "no-bitwise": true, + "no-bitwise": false, "no-conditional-assignment": true, - "no-consecutive-blank-lines": false, + "no-consecutive-blank-lines": true, "no-console": [ true, + "log", "debug", "info", "time", @@ -50,17 +55,15 @@ "no-construct": true, "no-constructor-vars": false, "no-debugger": true, - "no-duplicate-key": true, "no-duplicate-variable": true, - "no-empty": true, + "no-empty": false, "no-eval": true, "no-inferrable-types": false, "no-internal-module": true, - "no-require-imports": true, + "no-require-imports": false, "no-shadowed-variable": true, "no-switch-case-fall-through": true, "no-trailing-whitespace": true, - "no-unreachable": true, "no-unused-expression": true, "no-unused-variable": true, "no-use-before-declare": true, @@ -111,12 +114,48 @@ "allow-leading-underscore", "ban-keywords" ], + "callable-types": true, + "import-blacklist": [ + true, + "rxjs" + ], + "import-spacing": true, + "interface-over-type-literal": true, + "member-access": false, + "no-empty-interface": true, + "no-string-literal": false, + "no-string-throw": true, + "prefer-const": false, + "typeof-compare": true, + "unified-signatures": true, "whitespace": [ true, "check-branch", "check-decl", "check-operator", - "check-separator" - ] + "check-separator", + "check-typecast", + "check-type", + "check-typecast", + "check-module" + ], + "component-selector": [ + true, + "element", + "adf", + "kebab-case" + ], + "ordered-imports": false, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": false, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "no-access-missing-member": false, + "adf-file-name": true, + "adf-class-name": true, + "adf-prefix-name": true } }