[ADF-4857] Be able to run ng build content-service (#5183)

* Be able to build conten with ng build content-service

* fix tslint

* The translate module is necessary

* Rollback the build commands

* Rollback

* Remove wrong imports

* Trigger the build
This commit is contained in:
Maurizio Vitale
2019-10-24 19:23:30 +01:00
committed by Denys Vuika
parent 9fa1db063a
commit 6331979baa
401 changed files with 88 additions and 99 deletions

View File

@@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="566px" height="165px" viewBox="0 0 566 165" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>empty_doc_lib</title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="5.68434189e-14" y="-1.01962883e-12" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-2">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-3" x="-4.54747351e-13" y="5.68434189e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-4">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-5" x="-1.08002496e-12" y="7.81597009e-14" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-6">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-7" x="1.29318778e-12" y="9.23705556e-14" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-8">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-9" x="-2.96651592e-13" y="-7.60280727e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-10">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-11" x="3.48165941e-13" y="2.27373675e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-12">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-13" x="0" y="-5.40012479e-13" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-14">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
<rect id="path-15" x="0" y="0" width="78.1679389" height="78.1679389" rx="2"></rect>
<filter x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox" id="filter-16">
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2"></feOffset>
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter2" result="shadowBlurOuter2"></feGaussianBlur>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" type="matrix" in="shadowBlurOuter2" result="shadowMatrixOuter2"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="shadowMatrixOuter2"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="empty-folder-state-desktop" transform="translate(-37.000000, -168.000000)">
<g id="empty_doc_lib" transform="translate(38.000000, 169.000000)">
<g id="Group-5" transform="translate(241.569490, 92.634375) rotate(-355.000000) translate(-241.569490, -92.634375) translate(202.069490, 53.134375)">
<g id="Rectangle-1196-Copy-2">
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
</g>
<g id="filetype_video" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M39.0839695,21.9847328 L43.9694656,21.9847328 L43.9694656,17.0992366 L39.0839695,17.0992366 L39.0839695,21.9847328 Z M39.0839695,31.7557252 L43.9694656,31.7557252 L43.9694656,26.870229 L39.0839695,26.870229 L39.0839695,31.7557252 Z M39.0839695,41.5267176 L43.9694656,41.5267176 L43.9694656,36.6412214 L39.0839695,36.6412214 L39.0839695,41.5267176 Z M14.6564885,21.9847328 L19.5419847,21.9847328 L19.5419847,17.0992366 L14.6564885,17.0992366 L14.6564885,21.9847328 Z M14.6564885,31.7557252 L19.5419847,31.7557252 L19.5419847,26.870229 L14.6564885,26.870229 L14.6564885,31.7557252 Z M14.6564885,41.5267176 L19.5419847,41.5267176 L19.5419847,36.6412214 L14.6564885,36.6412214 L14.6564885,41.5267176 Z M43.9694656,7.32824427 L43.9694656,12.2137405 L39.0839695,12.2137405 L39.0839695,7.32824427 L19.5419847,7.32824427 L19.5419847,12.2137405 L14.6564885,12.2137405 L14.6564885,7.32824427 L9.77099237,7.32824427 L9.77099237,51.2977099 L14.6564885,51.2977099 L14.6564885,46.4122137 L19.5419847,46.4122137 L19.5419847,51.2977099 L39.0839695,51.2977099 L39.0839695,46.4122137 L43.9694656,46.4122137 L43.9694656,51.2977099 L48.8549618,51.2977099 L48.8549618,7.32824427 L43.9694656,7.32824427 Z" id="Fill-2" fill="#FFC107"></path>
</g>
</g>
<g id="Group-7" transform="translate(515.948329, 81.354522) rotate(-345.000000) translate(-515.948329, -81.354522) translate(476.448329, 41.854522)">
<g id="Rectangle-1196-Copy-3">
<use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-3"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-3"></use>
</g>
<g id="filetype_image" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M20.7636031,32.9773435 L26.8704733,40.3178015 L35.4200916,29.3132214 L46.412458,43.9697099 L12.2139847,43.9697099 L20.7636031,32.9773435 Z M51.2979542,46.412458 L51.2979542,12.2139847 C51.2979542,9.51474809 49.1116947,7.32848855 46.412458,7.32848855 L12.2139847,7.32848855 C9.51474809,7.32848855 7.32848855,9.51474809 7.32848855,12.2139847 L7.32848855,46.412458 C7.32848855,49.1116947 9.51474809,51.2979542 12.2139847,51.2979542 L46.412458,51.2979542 C49.1116947,51.2979542 51.2979542,49.1116947 51.2979542,46.412458 L51.2979542,46.412458 Z" id="Fill-2" fill="#22BE73"></path>
</g>
</g>
<g id="Group-8" transform="translate(309.051884, 62.261808) rotate(-5.000000) translate(-309.051884, -62.261808) translate(269.551884, 22.761808)">
<g id="Rectangle-1196-Copy-4">
<use fill="black" fill-opacity="1" filter="url(#filter-6)" xlink:href="#path-5"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-5"></use>
</g>
<g id="filetype_googledocs" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="6.82121026e-13 58.6259542 58.6259542 58.6259542 58.6259542 -2.98427949e-13 6.82121026e-13 -2.98427949e-13"></polygon>
<g id="Group-6" transform="translate(9.770992, 4.885496)">
<path d="M7.32824427,21.9847328 L31.7557252,21.9847328 L31.7557252,19.5419847 L7.32824427,19.5419847 L7.32824427,21.9847328 Z M7.32824427,26.870229 L31.7557252,26.870229 L31.7557252,24.4274809 L7.32824427,24.4274809 L7.32824427,26.870229 Z M7.32824427,31.7557252 L31.7557252,31.7557252 L31.7557252,29.3129771 L7.32824427,29.3129771 L7.32824427,31.7557252 Z M7.32824427,36.6412214 L21.9847328,36.6412214 L21.9847328,34.1984733 L7.32824427,34.1984733 L7.32824427,36.6412214 Z M29.3129771,0 L4.88549618,0 C2.18625954,0 0.0244274809,2.18625954 0.0244274809,4.88549618 L0,43.9694656 C0,46.6687023 2.16183206,48.8549618 4.8610687,48.8549618 L34.1984733,48.8549618 C36.8977099,48.8549618 39.0839695,46.6687023 39.0839695,43.9694656 L39.0839695,9.77099237 L29.3129771,0 Z" id="Fill-2" fill="#2979FF"></path>
<polygon id="Fill-4" fill-opacity="0.5" fill="#FFFFFF" points="29.3129771 9.77099237 29.3129771 -2.84217094e-14 39.0839695 9.77099237"></polygon>
<polygon id="Fill-5" fill-opacity="0.2" fill="#000000" points="39.0839695 9.77099237 39.0839695 19.5419847 29.3129771 9.77099237"></polygon>
</g>
</g>
</g>
<g id="Group-9" transform="translate(155.408682, 49.364493) rotate(-345.000000) translate(-155.408682, -49.364493) translate(115.908682, 9.864493)">
<g id="Rectangle-1196-Copy-5">
<use fill="black" fill-opacity="1" filter="url(#filter-8)" xlink:href="#path-7"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-7"></use>
</g>
<g id="filetype_pdf" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M45.1888855,25.5877863 L45.1888855,21.9187786 L37.853313,21.9187786 L37.853313,36.5923664 L41.5198779,36.5923664 L41.5198779,31.7777099 L45.1888855,31.7777099 L45.1888855,28.1087023 L41.5198779,28.1087023 L41.5198779,25.5877863 L45.1888855,25.5877863 Z M29.3696489,32.9233588 L31.7757557,32.9233588 L31.7757557,25.5877863 L29.3696489,25.5877863 L29.3696489,32.9233588 Z M35.4447634,32.9233588 L35.4447634,25.5877863 C35.4447634,24.5960305 35.1027786,23.7361832 34.4139237,23.0082443 C33.7275115,22.2827481 32.8481221,21.9187786 31.7781985,21.9187786 L25.703084,21.9187786 L25.703084,36.5923664 L31.7781985,36.5923664 C32.8481221,36.5923664 33.7275115,36.2308397 34.4139237,35.5029008 C35.1027786,34.7774046 35.4447634,33.9175573 35.4447634,32.9233588 L35.4447634,32.9233588 Z M17.1070534,28.1087023 L19.5131603,28.1087023 L19.5131603,25.5877863 L17.1070534,25.5877863 L17.1070534,28.1087023 Z M23.1821679,28.1087023 L23.1821679,25.5877863 C23.1821679,24.5960305 22.8181985,23.7361832 22.0927023,23.0082443 C21.3672061,22.2827481 20.5073588,21.9187786 19.5131603,21.9187786 L13.4380458,21.9187786 L13.4380458,36.5923664 L17.1070534,36.5923664 L17.1070534,31.7777099 L19.5131603,31.7777099 C20.5073588,31.7777099 21.3672061,31.4161832 22.0927023,30.6882443 C22.8181985,29.9627481 23.1821679,29.1029008 23.1821679,28.1087023 L23.1821679,28.1087023 Z M46.483542,7.32824427 C47.783084,7.32824427 48.9091908,7.8070229 49.8643053,8.7621374 C50.8218626,9.71725191 51.2981985,10.8433588 51.2981985,12.1429008 L51.2981985,46.3682443 C51.2981985,47.670229 50.8218626,48.8158779 49.8643053,49.8076336 C48.9091908,50.8018321 47.783084,51.2977099 46.483542,51.2977099 L12.2581985,51.2977099 C10.9586565,51.2977099 9.81300763,50.8018321 8.81880916,49.8076336 C7.82461069,48.8158779 7.32873282,47.670229 7.32873282,46.3682443 L7.32873282,12.1429008 C7.32873282,10.8433588 7.82461069,9.71725191 8.81880916,8.7621374 C9.81300763,7.8070229 10.9586565,7.32824427 12.2581985,7.32824427 L46.483542,7.32824427 Z" id="Fill-2" fill="#E91E63"></path>
</g>
</g>
<g id="Group-12" transform="translate(49.364493, 62.584254) rotate(-15.000000) translate(-49.364493, -62.584254) translate(9.864493, 23.084254)">
<g id="Rectangle-1196-Copy-7">
<use fill="black" fill-opacity="1" filter="url(#filter-10)" xlink:href="#path-9"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-9"></use>
</g>
<g id="filetype_forms" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M24.4274809,24.4250382 L41.5267176,24.4250382 L41.5267176,19.539542 L24.4274809,19.539542 L24.4274809,24.4250382 Z M24.4274809,31.7532824 L41.5267176,31.7532824 L41.5267176,26.8677863 L24.4274809,26.8677863 L24.4274809,31.7532824 Z M24.4274809,39.0839695 L41.5267176,39.0839695 L41.5267176,34.1984733 L24.4274809,34.1984733 L24.4274809,39.0839695 Z M17.0992366,24.4250382 L21.9847328,24.4250382 L21.9847328,19.539542 L17.0992366,19.539542 L17.0992366,24.4250382 Z M17.0992366,31.7532824 L21.9847328,31.7532824 L21.9847328,26.8677863 L17.0992366,26.8677863 L17.0992366,31.7532824 Z M17.0992366,39.0839695 L21.9847328,39.0839695 L21.9847328,34.1984733 L17.0992366,34.1984733 L17.0992366,39.0839695 Z M43.9694656,9.77099237 L14.6564885,9.77099237 C11.9694656,9.77099237 9.77099237,11.9694656 9.77099237,14.6564885 L9.77099237,43.9694656 C9.77099237,46.6564885 11.9694656,48.8549618 14.6564885,48.8549618 L43.9694656,48.8549618 C46.6564885,48.8549618 48.8549618,46.6564885 48.8549618,43.9694656 L48.8549618,14.6564885 C48.8549618,11.9694656 46.6564885,9.77099237 43.9694656,9.77099237 L43.9694656,9.77099237 Z" id="Fill-2" fill="#651FFF"></path>
</g>
</g>
<g id="Group-11" transform="translate(107.814782, 114.998541) rotate(-10.000000) translate(-107.814782, -114.998541) translate(68.314782, 75.498541)">
<g id="Rectangle-1196-Copy-6">
<use fill="black" fill-opacity="1" filter="url(#filter-12)" xlink:href="#path-11"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-11"></use>
</g>
<g id="filetype_excel" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="-5.68434189e-14 58.6259542 58.6259542 58.6259542 58.6259542 -1.56319402e-13 -5.68434189e-14 -1.56319402e-13"></polygon>
<g id="Group-10" transform="translate(4.885496, 4.885496)" fill="#22BE73">
<path d="M47.1621374,41.632 L28.848855,41.632 L28.848855,38.3025344 L33.2873282,38.3025344 L33.2873282,34.4161221 L28.848855,34.4161221 L28.848855,32.1981069 L33.2873282,32.1981069 L33.2873282,28.3116947 L28.848855,28.3116947 L28.848855,26.0936794 L33.2873282,26.0936794 L33.2873282,22.2072672 L28.848855,22.2072672 L28.848855,19.9868092 L33.2873282,19.9868092 L33.2873282,16.1028397 L28.848855,16.1028397 L28.848855,13.8823817 L33.2873282,13.8823817 L33.2873282,9.99841221 L28.848855,9.99841221 L28.848855,6.66894656 L47.1621374,6.66894656 L47.1621374,41.632 L47.1621374,41.632 Z M16.3444275,33.1141374 C15.4015267,30.7984122 14.2509924,28.5632977 13.5743511,26.1425344 C12.819542,28.3947481 11.7422901,30.5223817 10.8775573,32.730626 C9.6610687,32.7135267 8.4470229,32.6646718 7.23053435,32.613374 C8.65709924,29.821313 10.0348092,27.0072672 11.5053435,24.2323053 C10.2546565,21.3742901 8.88427481,18.572458 7.59694656,15.731542 C8.81832061,15.6582595 10.0396947,15.5874198 11.2610687,15.5190229 C12.0867176,17.690626 12.9929771,19.832916 13.6745038,22.0582595 C14.4073282,19.6985649 15.5016794,17.4781069 16.4372519,15.1990229 C17.6928244,15.1086412 18.9532824,15.032916 20.2112977,14.9718473 C18.7309924,18.0057405 17.2433588,21.0420763 15.7337405,24.0661985 C17.260458,27.1758168 18.8189313,30.2610076 20.3505344,33.3681832 C19.0143511,33.2900153 17.6806107,33.2069618 16.3444275,33.1141374 L16.3444275,33.1141374 Z M48.8329771,37.2203969 C48.8280916,27.591084 48.8158779,17.961771 48.8427481,8.32757252 C48.8036641,7.38467176 48.8720611,6.34161832 48.300458,5.51841221 C47.4845802,4.9590229 46.4512977,5.0249771 45.5132824,4.98589313 C39.9584733,5.01520611 34.4036641,5.00299237 28.848855,5.00299237 L28.848855,0.564519084 L25.551145,0.564519084 C17.0381679,2.06680916 8.5178626,3.52757252 4.68958206e-13,5.00787786 L4.68958206e-13,43.852458 C8.46900763,45.3327634 16.9429008,46.7544427 25.4021374,48.2909313 L28.848855,48.2909313 L28.848855,43.2979542 C34.6039695,43.2857405 40.359084,43.3126107 46.1068702,43.2979542 C47.0351145,43.2588702 48.4225954,43.2295573 48.6448855,42.0765802 C48.9819847,40.4839084 48.8036641,38.8350534 48.8329771,37.2203969 L48.8329771,37.2203969 Z" id="Fill-2"></path>
<path d="M35.5077863,13.8821374 L43.2781679,13.8821374 L43.2781679,9.99816794 L35.5077863,9.99816794 L35.5077863,13.8821374 Z M35.5077863,19.9865649 L43.2781679,19.9865649 L43.2781679,16.1025954 L35.5077863,16.1025954 L35.5077863,19.9865649 Z M35.5077863,26.0909924 L43.2781679,26.0909924 L43.2781679,22.2070229 L35.5077863,22.2070229 L35.5077863,26.0909924 Z M35.5077863,32.1954198 L43.2781679,32.1954198 L43.2781679,28.3114504 L35.5077863,28.3114504 L35.5077863,32.1954198 Z M35.5077863,38.3022901 L43.2781679,38.3022901 L43.2781679,34.4183206 L35.5077863,34.4183206 L35.5077863,38.3022901 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
<g id="Group-4" transform="translate(388.820630, 86.064353) rotate(-350.000000) translate(-388.820630, -86.064353) translate(349.320630, 46.564353)">
<g id="Rectangle-1196-Copy">
<use fill="black" fill-opacity="1" filter="url(#filter-14)" xlink:href="#path-13"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-13"></use>
</g>
<g id="filetype_audio" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="-1.13686838e-13 58.6259542 58.6259542 58.6259542 58.6259542 -6.39488462e-14 -1.13686838e-13 -6.39488462e-14"></polygon>
<path d="M29.3129771,7.32824427 L43.9694656,7.32824427 L43.9694656,17.0601527 L34.2375573,17.0601527 L34.2375573,41.5658015 C34.2375573,44.2381679 33.2629008,46.5270229 31.3160305,48.4348092 C29.3691603,50.3450382 27.0607634,51.2977099 24.3883969,51.2977099 C21.7160305,51.2977099 19.4271756,50.3450382 17.5193893,48.4348092 C15.6091603,46.5270229 14.6564885,44.2381679 14.6564885,41.5658015 C14.6564885,38.8934351 15.6091603,36.5850382 17.5193893,34.6381679 C19.4271756,32.6912977 21.7160305,31.7166412 24.3883969,31.7166412 C25.9932824,31.7166412 27.6323664,32.1758779 29.3129771,33.0919084 L29.3129771,7.32824427 Z" id="Fill-2" fill="#E91E63"></path>
</g>
</g>
<g id="Group-2" transform="translate(411.603053, 1.221374)">
<g id="Group-3">
<g id="Rectangle-1196">
<use fill="black" fill-opacity="1" filter="url(#filter-16)" xlink:href="#path-15"></use>
<use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-15"></use>
</g>
<polygon id="Fill-1" points="9.77099237 68.3969466 68.3969466 68.3969466 68.3969466 9.77099237 9.77099237 9.77099237"></polygon>
</g>
<g id="filetype_book" transform="translate(9.770992, 9.770992)">
<polygon id="Fill-1" points="0 58.6259542 58.6259542 58.6259542 58.6259542 0 0 0"></polygon>
<path d="M14.6564885,9.77099237 L26.870229,9.77099237 L26.870229,29.3129771 L20.7633588,25.648855 L14.6564885,29.3129771 L14.6564885,9.77099237 Z M43.9694656,4.88549618 L14.6564885,4.88549618 C11.9572519,4.88549618 9.77099237,7.07175573 9.77099237,9.77099237 L9.77099237,48.8549618 C9.77099237,51.5541985 11.9572519,53.740458 14.6564885,53.740458 L43.9694656,53.740458 C46.6687023,53.740458 48.8549618,51.5541985 48.8549618,48.8549618 L48.8549618,9.77099237 C48.8549618,7.07175573 46.6687023,4.88549618 43.9694656,4.88549618 L43.9694656,4.88549618 Z" id="Fill-2" fill="#FF6D40"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,78 @@
<nav
*ngIf="folderNode"
data-automation-id="breadcrumb"
class="adf-breadcrumb-container"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
<button
*ngIf="hasPreviousNodes()"
tabindex="0"
class="adf-breadcrumb-dropdown-trigger"
(click)="open()"
>
<div class="adf-breadcrumb-dropdown-trigger-icon">
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()">folder</mat-icon>
<mat-icon
[class.adf-isRoot]="!hasPreviousNodes()"
class="adf-breadcrumb-dropdown-trigger-arrow"
>arrow_drop_down</mat-icon
>
</div>
</button>
<mat-select
#dropdown
*ngIf="hasPreviousNodes()"
class="adf-breadcrumb-dropdown-path"
tabindex="0"
>
<mat-option
*ngFor="let node of previousNodes"
(click)="onRoutePathClick(node, $event)"
class="adf-breadcrumb-path-option"
tabindex="0"
>
{{ node.name | translate }}
</mat-option>
</mat-select>
<div
*ngFor="let item of lastNodes; let last = last"
[class.adf-active]="last"
[ngSwitch]="last"
title="{{ item.name | translate }}"
class="adf-breadcrumb-item">
<a
*ngSwitchDefault
href="#"
[attr.data-automation-id]="'breadcrumb_' + item.name"
class="adf-breadcrumb-item-anchor"
(click)="onRoutePathClick(item, $event)"
>
{{ item.name | translate }}
</a>
<div *ngSwitchCase="true" class="adf-breadcrumb-item-current"
[attr.aria-current]="'BREADCRUMB.ARIA-LABEL.CURRENT_PAGE' | translate">
{{ item.name | translate }}
</div>
<mat-icon class="adf-breadcrumb-item-chevron" *ngIf="!last">
chevron_right
</mat-icon>
</div>
</nav>
<nav
*ngIf="!folderNode && hasRoot"
data-automation-id="breadcrumb"
role="navigation"
[attr.aria-label]="'BREADCRUMB.ARIA-LABEL.BREADCRUMB' | translate"
>
<div class="adf-breadcrumb-item adf-active" role="listitem">
<div class="adf-breadcrumb-item-current">
{{ root | translate }}
</div>
</div>
</nav>

View File

@@ -0,0 +1,157 @@
@mixin adf-breadcrumb-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$breadcrumb-chevron-spacer: 2px;
$breadcrumb-outline: 1px solid mat-color($alfresco-ecm-blue, A200) !default;
.adf-breadcrumb {
display: flex;
flex: 1;
line-height: 24px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
color: mat-color($foreground, text, 0.54);
overflow: hidden;
&-container {
margin: 0;
padding: 0;
list-style-type: none;
cursor: default;
display: flex;
overflow: hidden;
}
&-dropdown {
&-path {
width: 0;
height: 0;
overflow: hidden;
margin-top: 35px;
&.mat-select {
width: 0;
}
}
&-trigger {
cursor: pointer;
padding: 0;
border: none;
background: transparent;
width: 30px;
margin-top: 2px;
margin-right: 5px;
&:focus {
color: mat-color($primary);
outline: none;
}
&-icon {
position: relative;
}
&-arrow {
font-size: 17px;
position: absolute;
left: 4px;
top: 4px;
color: white;
z-index: 2;
}
&-arrow.adf-isRoot {
visibility: hidden;
}
&-arrow.adf-focus {
border: none;
}
}
&-trigger.adf-isRoot {
cursor: not-allowed;
}
}
&-item {
padding-right: $breadcrumb-chevron-spacer;
overflow: hidden;
display: flex;
line-height: 33px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
text-align: left;
color: mat-color($foreground, text, 0.54);
flex: 0 1 auto;
min-width: 35px;
margin-top: auto;
text-overflow: ellipsis;
&:hover,
&.adf-active {
color: mat-color($foreground, text, 0.64);
}
&.adf-active {
color: mat-color($foreground, text, 0.87);
}
&-chevron {
opacity: 1;
margin-top: 9px;
font-size: 17px;
}
&.mat-primary {
color: mat-color($primary);
}
&.mat-accent {
color: mat-color($accent);
}
&.mat-warn {
color: mat-color($warn);
}
&-anchor {
box-sizing: border-box;
color: inherit;
text-decoration: none;
display: inline-block;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex: 0 1 auto;
padding: 0 2px;
text-align: center;
&:focus {
outline: $breadcrumb-outline;
outline-offset: -1px;
}
}
&-current {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
&-path-option {
&.mat-option {
background-color: mat-color($background, card);
}
}
}
}

View File

@@ -0,0 +1,223 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { PathElementEntity } from '@alfresco/js-api';
import { setupTestBed } from '@alfresco/adf-core';
import { fakeNodeWithCreatePermission } from '../mock';
import { DocumentListComponent, DocumentListService } from '../document-list';
import { BreadcrumbComponent } from './breadcrumb.component';
import { ContentTestingModule } from '../testing/content.testing.module';
import { of } from 'rxjs';
describe('Breadcrumb', () => {
let component: BreadcrumbComponent;
let fixture: ComponentFixture<BreadcrumbComponent>;
let documentListService: DocumentListService = jasmine.createSpyObj({'loadFolderByNodeId' : of(''), 'isCustomSourceService': false});
let documentListComponent: DocumentListComponent;
setupTestBed({
imports: [ContentTestingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers : [{ provide: DocumentListService, useValue: documentListService }]
});
beforeEach(() => {
fixture = TestBed.createComponent(BreadcrumbComponent);
component = fixture.componentInstance;
documentListComponent = TestBed.createComponent<DocumentListComponent>(DocumentListComponent).componentInstance;
documentListService = TestBed.get(DocumentListService);
});
afterEach(() => {
fixture.destroy();
});
it('should prevent default click behavior', () => {
const event = jasmine.createSpyObj('event', ['preventDefault']);
component.onRoutePathClick(null, event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should root be present as default node if the path is null', () => {
component.root = 'default';
component.folderNode = fakeNodeWithCreatePermission;
component.ngOnChanges();
expect(component.route[0].name).toBe('default');
});
it('should emit navigation event', (done) => {
const node = <PathElementEntity> { id: '-id-', name: 'name' };
component.navigate.subscribe((val) => {
expect(val).toBe(node);
done();
});
component.onRoutePathClick(node, null);
});
it('should update document list on click', () => {
const node = <PathElementEntity> { id: '-id-', name: 'name' };
component.target = documentListComponent;
component.onRoutePathClick(node, null);
expect(documentListService.loadFolderByNodeId).toHaveBeenCalledWith(node.id,
documentListComponent.DEFAULT_PAGINATION,
documentListComponent.includeFields,
documentListComponent.where);
});
it('should not parse the route when node not provided', () => {
expect(component.parseRoute(null)).toEqual([]);
});
it('should not parse the route when node has no path', () => {
const node: any = {};
expect(component.parseRoute(node)).toEqual([]);
});
it('should append the node to the route', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-id', name: 'element-name' }
]
}
};
const route = component.parseRoute(node);
expect(route.length).toBe(2);
expect(route[1].id).toBe(node.id);
expect(route[1].name).toBe(node.name);
});
it('should trim the route if custom root id provided', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.rootId = 'element-2-id';
const route = component.parseRoute(node);
expect(route.length).toBe(3);
expect(route[0].id).toBe('element-2-id');
expect(route[0].name).toBe('element-2-name');
expect(route[2].id).toBe(node.id);
expect(route[2].name).toBe(node.name);
});
it('should rename root node if custom name provided', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.root = 'custom root';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('element-1-id');
expect(route[0].name).toBe('custom root');
});
it('should replace root id if nothing to trim in the path', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.rootId = 'custom-id';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('custom-id');
expect(route[0].name).toBe('element-1-name');
});
it('should replace both id and name of the root element', () => {
const node: any = {
id: 'test-id',
name: 'test-name',
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.root = 'custom-name';
component.rootId = 'custom-id';
const route = component.parseRoute(node);
expect(route.length).toBe(4);
expect(route[0].id).toBe('custom-id');
expect(route[0].name).toBe('custom-name');
});
it('should apply the transformation function when there is one', () => {
const node: any = {
id: null,
name: null,
path: {
elements: [
{ id: 'element-1-id', name: 'element-1-name' },
{ id: 'element-2-id', name: 'element-2-name' },
{ id: 'element-3-id', name: 'element-3-name' }
]
}
};
component.transform = ((transformNode) => {
transformNode.id = 'test-id';
transformNode.name = 'test-name';
return transformNode;
});
component.folderNode = node;
component.ngOnChanges();
expect(component.route.length).toBe(4);
expect(component.route[3].id).toBe('test-id');
expect(component.route[3].name).toBe('test-name');
});
});

View File

@@ -0,0 +1,198 @@
/*!
* @license
* Copyright 2019 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,
OnChanges,
OnInit,
Output,
ViewChild,
ViewEncapsulation,
OnDestroy
} from '@angular/core';
import { MatSelect } from '@angular/material';
import { Node, PathElementEntity } from '@alfresco/js-api';
import { DocumentListComponent } from '../document-list';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
host: {
'class': 'adf-breadcrumb'
}
})
export class BreadcrumbComponent implements OnInit, OnChanges, OnDestroy {
/** Active node, builds UI based on folderNode.path.elements collection. */
@Input()
folderNode: Node = 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;
/** Transformation to be performed on the chosen/folder node before building
* the breadcrumb UI. Can be useful when custom formatting is needed for the
* breadcrumb. You can change the path elements from the node that are used to
* build the breadcrumb using this function.
*/
@Input()
transform: (node) => any;
@ViewChild('dropdown')
dropdown: MatSelect;
/** Maximum number of nodes to display before wrapping them with a dropdown element. */
@Input()
maxItems: number;
previousNodes: PathElementEntity[];
lastNodes: PathElementEntity[];
route: PathElementEntity[] = [];
private onDestroy$ = new Subject<boolean>();
get hasRoot(): boolean {
return !!this.root;
}
/** Emitted when the user clicks on a breadcrumb. */
@Output()
navigate = new EventEmitter<PathElementEntity>();
ngOnInit() {
this.transform = this.transform ? this.transform : null;
if (this.target) {
this.target.$folderNode
.pipe(takeUntil(this.onDestroy$))
.subscribe((folderNode: Node) => {
this.folderNode = folderNode;
this.recalculateNodes();
});
}
}
ngOnChanges(): void {
this.recalculateNodes();
}
protected recalculateNodes(): void {
const node: Node = this.transform ? this.transform(this.folderNode) : this.folderNode;
this.route = this.parseRoute(node);
if (this.maxItems && this.route.length > this.maxItems) {
this.lastNodes = this.route.slice(this.route.length - this.maxItems);
this.previousNodes = this.route.slice(0, this.route.length - this.maxItems);
this.previousNodes.reverse();
} else {
this.lastNodes = this.route;
this.previousNodes = null;
}
}
open(): void {
if (this.dropdown) {
this.dropdown.open();
}
}
hasPreviousNodes(): boolean {
return this.previousNodes ? true : false;
}
parseRoute(node: Node): PathElementEntity[] {
if (node && node.path) {
const route = <PathElementEntity[]> (node.path.elements || []).slice();
route.push(<PathElementEntity> {
id: node.id,
name: node.name,
node: node
});
const rootPos = this.getElementPosition(route, this.rootId);
if (rootPos > 0) {
route.splice(0, rootPos);
}
if (rootPos === -1 && this.rootId) {
route[0].id = this.rootId;
}
if (this.root) {
route[0].name = this.root;
}
return route;
}
return [];
}
private getElementPosition(route: PathElementEntity[], nodeId: string): number {
let position: number = -1;
if (route && route.length > 0 && nodeId) {
position = route.findIndex((el) => el.id === nodeId);
}
return position;
}
onRoutePathClick(route: PathElementEntity, event?: Event): void {
if (event) {
event.preventDefault();
}
if (route) {
this.navigate.emit(route);
if (this.target) {
this.target.navigateTo(route.id);
}
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
}

View File

@@ -0,0 +1,41 @@
/*!
* @license
* Copyright 2019 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 { MaterialModule } from '../material.module';
import { BreadcrumbComponent } from './breadcrumb.component';
import { DropdownBreadcrumbComponent } from './dropdown-breadcrumb.component';
import { CoreModule } from '@alfresco/adf-core';
@NgModule({
imports: [
CommonModule,
MaterialModule,
CoreModule
],
exports: [
BreadcrumbComponent,
DropdownBreadcrumbComponent
],
declarations: [
BreadcrumbComponent,
DropdownBreadcrumbComponent
]
})
export class BreadcrumbModule {}

View File

@@ -0,0 +1,36 @@
<ng-container *ngIf="route.length > 0" class="adf-dropdown-breadcrumb-container">
<button
tabindex="0"
class="adf-dropdown-breadcrumb-trigger"
(click)="open()"
data-automation-id="dropdown-breadcrumb-trigger">
<mat-icon [class.adf-isRoot]="!hasPreviousNodes()">folder</mat-icon>
</button>
<mat-icon class="adf-dropdown-breadcrumb-item-chevron">chevron_right</mat-icon>
<div class="adf-dropdown-breadcrumb-path">
<mat-select
#dropdown
*ngIf="hasPreviousNodes()"
tabindex="0"
data-automation-id="dropdown-breadcrumb-path">
<mat-option
*ngFor="let node of previousNodes;"
(click)="onRoutePathClick(node, $event)"
class="adf-dropdown-breadcrumb-path-option"
tabindex="0"
data-automation-class="dropdown-breadcrumb-path-option">
{{ node.name | translate }}
</mat-option>
</mat-select>
</div>
<span
class="adf-current-folder"
[class.adf-isRoot]="!hasPreviousNodes()"
data-automation-id="current-folder">{{ currentNode.name }}
</span>
</ng-container>

View File

@@ -0,0 +1,102 @@
@mixin adf-breadcrumb-dropdown-theme($theme) {
$primary: map-get($theme, primary);
$dropdownHorizontalOffset: 30px;
$foreground: map-get($theme, foreground);
.adf {
&-dropdown-breadcrumb {
display: flex;
flex: 1;
line-height: 24px;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
color: mat-color($foreground, text, 0.54);
overflow: hidden;
margin-top: 10px;
.mat-icon {
height: 35px;
}
&-container {
margin: 0;
padding: 0;
list-style-type: none;
cursor: default;
display: flex;
overflow: hidden;
}
}
&-dropdown-breadcrumb-trigger {
cursor: pointer;
padding: 0;
border: none;
background: transparent;
width: 25px;
&:focus {
outline: none;
}
}
&-dropdown-breadcrumb-trigger.adf-isRoot {
cursor: not-allowed;
}
&-dropdown-breadcrumb-path {
width: 0;
height: 0;
overflow: hidden;
margin-top: 35px;
&.mat-select {
width: 0;
}
}
&-current-folder {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
width: 75%;
}
&-dropdown-breadcrumb-path-option.mat-option {
height: 28px;
line-height: 28px;
padding: 0 12px;
font-size: 13px;
}
&-dropdown-breadcrumb-path-option.mat-option:first-child {
padding-top: 4px;
}
&-dropdown-breadcrumb-path-option.mat-option:last-child {
padding-bottom: 4px;
}
}
[dir='ltr'] .adf {
&-dropdown-breadcrumb-path {
margin-left: -$dropdownHorizontalOffset;
}
&-current-folder {
margin-left: $dropdownHorizontalOffset;
}
}
[dir='rtl'] .adf {
&-dropdown-breadcrumb-path {
margin-right: -$dropdownHorizontalOffset;
}
&-current-folder {
margin-right: $dropdownHorizontalOffset;
}
}
}

View File

@@ -0,0 +1,162 @@
/*!
* @license
* Copyright 2019 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 { setupTestBed } from '@alfresco/adf-core';
import { fakeNodeWithCreatePermission } from '../mock';
import { DocumentListComponent, DocumentListService } from '../document-list';
import { DropdownBreadcrumbComponent } from './dropdown-breadcrumb.component';
import { ContentTestingModule } from '../testing/content.testing.module';
import { of } from 'rxjs';
describe('DropdownBreadcrumb', () => {
let component: DropdownBreadcrumbComponent;
let fixture: ComponentFixture<DropdownBreadcrumbComponent>;
let documentList: DocumentListComponent;
let documentListService: DocumentListService = jasmine.createSpyObj({'loadFolderByNodeId' : of(''), 'isCustomSourceService': false});
setupTestBed({
imports: [ContentTestingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers : [{ provide: DocumentListService, useValue: documentListService }]
});
beforeEach(async(() => {
fixture = TestBed.createComponent(DropdownBreadcrumbComponent);
component = fixture.componentInstance;
documentList = TestBed.createComponent<DocumentListComponent>(DocumentListComponent).componentInstance;
documentListService = TestBed.get(DocumentListService);
}));
afterEach(async(() => {
fixture.destroy();
}));
function openSelect() {
const folderIcon = fixture.debugElement.nativeElement.querySelector('[data-automation-id="dropdown-breadcrumb-trigger"]');
folderIcon.click();
fixture.detectChanges();
}
function triggerComponentChange(fakeNodeData) {
component.folderNode = fakeNodeData;
component.ngOnChanges();
fixture.detectChanges();
}
function clickOnTheFirstOption() {
const option: any = document.querySelector('[id^="mat-option"]');
option.click();
}
it('should display only the current folder name if there is no previous folders', (done) => {
const fakeNodeWithCreatePermissionInstance = JSON.parse(JSON.stringify(fakeNodeWithCreatePermission));
fakeNodeWithCreatePermissionInstance.path.elements = [];
triggerComponentChange(fakeNodeWithCreatePermissionInstance);
fixture.whenStable().then(() => {
openSelect();
const currentFolder = fixture.debugElement.query(By.css('[data-automation-id="current-folder"]'));
const path = fixture.debugElement.query(By.css('[data-automation-id="dropdown-breadcrumb-path"]'));
expect(path).toBeNull();
expect(currentFolder).not.toBeNull();
expect(currentFolder.nativeElement.innerText.trim()).toEqual('Test');
done();
});
});
it('should display only the path in the selectBox', (done) => {
const fakeNodeWithCreatePermissionInstance = JSON.parse(JSON.stringify(fakeNodeWithCreatePermission));
fakeNodeWithCreatePermissionInstance.path.elements = [
{ id: '1', name: 'Stark Industries' },
{ id: '2', name: 'User Homes' },
{ id: '3', name: 'J.A.R.V.I.S' }
];
triggerComponentChange(fakeNodeWithCreatePermissionInstance);
fixture.whenStable().then(() => {
openSelect();
const path = fixture.debugElement.query(By.css('[data-automation-id="dropdown-breadcrumb-path"]'));
const options = fixture.debugElement.queryAll(By.css('[data-automation-class="dropdown-breadcrumb-path-option"]'));
expect(path).not.toBeNull();
expect(options.length).toBe(3);
done();
});
});
it('should emit navigation event when clicking on an option', (done) => {
const fakeNodeWithCreatePermissionInstance = JSON.parse(JSON.stringify(fakeNodeWithCreatePermission));
fakeNodeWithCreatePermissionInstance.path.elements = [{ id: '1', name: 'Stark Industries' }];
triggerComponentChange(fakeNodeWithCreatePermissionInstance);
fixture.whenStable().then(() => {
openSelect();
fixture.whenStable().then(() => {
component.navigate.subscribe((val) => {
expect(val).toEqual({ id: '1', name: 'Stark Industries' });
done();
});
clickOnTheFirstOption();
});
});
});
it('should update document list when clicking on an option', (done) => {
component.target = documentList;
const fakeNodeWithCreatePermissionInstance = JSON.parse(JSON.stringify(fakeNodeWithCreatePermission));
fakeNodeWithCreatePermissionInstance.path.elements = [{ id: '1', name: 'Stark Industries' }];
triggerComponentChange(fakeNodeWithCreatePermissionInstance);
fixture.whenStable().then(() => {
openSelect();
fixture.whenStable().then(() => {
clickOnTheFirstOption();
expect(documentListService.loadFolderByNodeId).toHaveBeenCalledWith('1', documentList.DEFAULT_PAGINATION, undefined, undefined);
done();
});
});
});
it('should open the selectBox when clicking on the folder icon', (done) => {
triggerComponentChange(JSON.parse(JSON.stringify(fakeNodeWithCreatePermission)));
spyOn(component.dropdown, 'open');
fixture.whenStable().then(() => {
openSelect();
fixture.whenStable().then(() => {
expect(component.dropdown.open).toHaveBeenCalled();
done();
});
});
});
});

View File

@@ -0,0 +1,66 @@
/*!
* @license
* Copyright 2019 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, OnChanges, ViewChild, ViewEncapsulation } from '@angular/core';
import { MatSelect } from '@angular/material';
import { PathElementEntity, Node } from '@alfresco/js-api';
import { BreadcrumbComponent } from './breadcrumb.component';
@Component({
selector: 'adf-dropdown-breadcrumb',
templateUrl: './dropdown-breadcrumb.component.html',
styleUrls: ['./dropdown-breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
host: {
'class': 'adf-dropdown-breadcrumb'
}
})
export class DropdownBreadcrumbComponent extends BreadcrumbComponent implements OnChanges {
@ViewChild('dropdown')
dropdown: MatSelect;
currentNode: PathElementEntity;
previousNodes: PathElementEntity[];
/**
* Calculate the current and previous nodes from the route array
*/
protected recalculateNodes(): void {
const node: Node = this.transform ? this.transform(this.folderNode) : this.folderNode;
this.route = this.parseRoute(node);
this.currentNode = this.route[this.route.length - 1];
this.previousNodes = this.route.slice(0, this.route.length - 1).reverse();
}
/**
* Opens the node picker menu
*/
open(): void {
if (this.dropdown) {
this.dropdown.open();
}
}
/**
* Return if route has more than one element (means: we are not in the root directory)
*/
hasPreviousNodes(): boolean {
return this.previousNodes.length > 0;
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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';

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 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 { Node } from '@alfresco/js-api';
export interface NavigableComponentInterface {
navigateTo(node: Node | string);
}

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 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 './breadcrumb.component';
export * from './dropdown-breadcrumb.component';
export * from './navigable-component.interface';
export * from './breadcrumb.module';

View File

@@ -0,0 +1,36 @@
<mat-card *ngIf="node">
<mat-card-content>
<adf-content-metadata
[displayDefaultProperties]="displayDefaultProperties"
[expanded]="expanded"
[node]="node"
[displayEmpty]="displayEmpty"
[editable]="editable"
[multi]="multi"
[displayAspect]="displayAspect"
[preset]="preset">
</adf-content-metadata>
</mat-card-content>
<mat-card-footer class="adf-content-metadata-card-footer" fxLayout="row" fxLayoutAlign="space-between stretch">
<div>
<button *ngIf="!readOnly && hasAllowableOperations()"
mat-icon-button
(click)="toggleEdit()"
[attr.title]="'CORE.METADATA.ACTIONS.EDIT' | translate"
[attr.aria-label]="'CORE.METADATA.ACTIONS.EDIT' | translate"
data-automation-id="meta-data-card-toggle-edit">
<mat-icon>mode_edit</mat-icon>
</button>
</div>
<button *ngIf="displayDefaultProperties" mat-button (click)="toggleExpanded()" data-automation-id="meta-data-card-toggle-expand">
<ng-container *ngIf="expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_down</mat-icon>
</ng-container>
<ng-container *ngIf="!expanded">
<span data-automation-id="meta-data-card-toggle-expand-label">{{ 'ADF_VIEWER.SIDEBAR.METADATA.LESS_INFORMATION' | translate }}</span>
<mat-icon>keyboard_arrow_up</mat-icon>
</ng-container>
</button>
</mat-card-footer>
</mat-card>

View File

@@ -0,0 +1,30 @@
@mixin adf-content-metadata-card-theme($theme) {
$primary: map-get($theme, primary);
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);
.adf-content-metadata-card {
.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);
button {
color: mat-color($foreground, text, 0.54);
&:hover {
color: mat-color($foreground, text, 0.87);
}
}
}
}
}

View File

@@ -0,0 +1,206 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Node } from '@alfresco/js-api';
import { ContentMetadataCardComponent } from './content-metadata-card.component';
import { ContentMetadataComponent } from '../content-metadata/content-metadata.component';
import { setupTestBed, AllowableOperationsEnum } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SimpleChange } from '@angular/core';
describe('ContentMetadataCardComponent', () => {
let component: ContentMetadataCardComponent;
let fixture: ComponentFixture<ContentMetadataCardComponent>;
let node: Node;
const preset = 'custom-preset';
setupTestBed({
imports: [ContentTestingModule]
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataCardComponent);
component = fixture.componentInstance;
node = <Node> {
aspectNames: [],
nodeType: '',
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 show more information when no metadata properties are being displayed', () => {
component.displayDefaultProperties = false;
expect(component.expanded).toBe(!component.displayDefaultProperties);
});
it('should show less information when metadata properties are being displayed', () => {
component.displayDefaultProperties = true;
expect(component.expanded).toBe(!component.displayDefaultProperties);
});
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 displayEmpty 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 multi to the underlying component', () => {
component.multi = true;
fixture.detectChanges();
const contentMetadataComponent = fixture.debugElement.query(By.directive(ContentMetadataComponent)).componentInstance;
expect(contentMetadataComponent.multi).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;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-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="meta-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="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.LESS_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="meta-data-card-toggle-expand-label"]'));
expect(buttonLabel.nativeElement.innerText.trim()).toBe('ADF_VIEWER.SIDEBAR.METADATA.MORE_INFORMATION');
});
it('should hide the edit button in readOnly is true', () => {
component.readOnly = true;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-edit"]'));
expect(button).toBeNull();
});
it('should hide the edit button if node does not have `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = null;
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-edit"]'));
expect(button).toBeNull();
});
it('should show the edit button if node does has `update` permissions', () => {
component.readOnly = false;
component.node.allowableOperations = [AllowableOperationsEnum.UPDATE];
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('[data-automation-id="meta-data-card-toggle-edit"]'));
expect(button).not.toBeNull();
});
it('should expand the card when custom display aspect is valid', () => {
expect(component.expanded).toBeFalsy();
let displayAspect = new SimpleChange(null , 'EXIF', true);
component.ngOnChanges({ displayAspect });
expect(component.expanded).toBeTruthy();
displayAspect = new SimpleChange('EXIF' , null, false);
component.ngOnChanges({ displayAspect });
expect(component.expanded).toBeTruthy();
});
});

View File

@@ -0,0 +1,107 @@
/*!
* @license
* Copyright 2019 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, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { ContentService, AllowableOperationsEnum } from '@alfresco/adf-core';
@Component({
selector: 'adf-content-metadata-card',
templateUrl: './content-metadata-card.component.html',
styleUrls: ['./content-metadata-card.component.scss'],
encapsulation: ViewEncapsulation.None,
host: { 'class': 'adf-content-metadata-card' }
})
export class ContentMetadataCardComponent implements OnChanges {
/** (required) The node entity to fetch metadata about */
@Input()
node: Node;
/** (optional) This flag displays/hides empty metadata
* fields.
*/
@Input()
displayEmpty: boolean = false;
/** (optional) This flag displays desired aspect when open for the first time
* fields.
*/
@Input()
displayAspect: string = null;
/** (required) Name of the metadata preset, which defines aspects
* and their properties.
*/
@Input()
preset: string;
/** (optional) This flag sets the metadata in read only mode
* preventing changes.
*/
@Input()
readOnly = false;
/** (optional) This flag allows the component to display more
* than one accordion at a time.
*/
@Input()
multi = false;
private _displayDefaultProperties: boolean = true;
/** (optional) This flag displays/hides the metadata
* properties.
*/
@Input()
set displayDefaultProperties(value: boolean) {
this._displayDefaultProperties = value;
this.onDisplayDefaultPropertiesChange();
}
get displayDefaultProperties(): boolean {
return this._displayDefaultProperties;
}
editable: boolean = false;
expanded: boolean;
constructor(private contentService: ContentService) {
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.displayAspect && changes.displayAspect.currentValue) {
this.expanded = true;
}
}
onDisplayDefaultPropertiesChange(): void {
this.expanded = !this._displayDefaultProperties;
}
toggleEdit(): void {
this.editable = !this.editable;
}
toggleExpanded(): void {
this.expanded = !this.expanded;
}
hasAllowableOperations() {
return this.contentService.hasAllowableOperations(this.node, AllowableOperationsEnum.UPDATE);
}
}

View File

@@ -0,0 +1,46 @@
<div class="adf-metadata-properties">
<mat-accordion displayMode="flat" [multi]="multi">
<mat-expansion-panel
*ngIf="displayDefaultProperties"
[expanded]="canExpandProperties()"
[attr.data-automation-id]="'adf-metadata-group-properties'" >
<mat-expansion-panel-header>
<mat-panel-title role="heading">
{{ 'CORE.METADATA.BASIC.HEADER' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
[properties]="basicProperties$ | async"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
<ng-container *ngIf="expanded">
<ng-container *ngIf="groupedProperties$ | async; else loading; let groupedProperties">
<div *ngFor="let group of groupedProperties; let first = first;" class="adf-metadata-grouped-properties-container">
<mat-expansion-panel *ngIf="showGroup(group) || editable"
[attr.data-automation-id]="'adf-metadata-group-' + group.title"
[expanded]="canExpandTheCard(group) || !displayDefaultProperties && first">
<mat-expansion-panel-header>
<mat-panel-title>
{{ group.title | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<adf-card-view
[properties]="group.properties"
[editable]="editable"
[displayEmpty]="displayEmpty">
</adf-card-view>
</mat-expansion-panel>
</div>
</ng-container>
<ng-template #loading>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</ng-container>
</mat-accordion>
</div>

View File

@@ -0,0 +1,21 @@
@mixin adf-content-metadata-theme($theme) {
$background: map-get($theme, background);
$panel-header-hover: mat-color($background, hover);
.adf {
&-metadata-properties {
.mat-expansion-panel-header.mat-expanded:hover,
.mat-expansion-panel-header.mat-expanded:focus {
background: $panel-header-hover;
}
mat-expansion-panel-header {
height: 64px;
}
.mat-expansion-panel:not([class*=mat-elevation-z]) {
box-shadow: none;
}
}
}
}

View File

@@ -0,0 +1,377 @@
/*!
* @license
* Copyright 2019 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 { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Node } from '@alfresco/js-api';
import { ContentMetadataComponent } from './content-metadata.component';
import { ContentMetadataService } from '../../services/content-metadata.service';
import {
CardViewBaseItemModel, CardViewComponent, CardViewUpdateService, NodesApiService,
LogService, setupTestBed
} from '@alfresco/adf-core';
import { throwError, of } from 'rxjs';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { mockGroupProperties } from './mock-data';
describe('ContentMetadataComponent', () => {
let component: ContentMetadataComponent;
let fixture: ComponentFixture<ContentMetadataComponent>;
let contentMetadataService: ContentMetadataService;
let updateService: CardViewUpdateService;
let nodesApiService: NodesApiService;
let node: Node;
let folderNode: Node;
const preset = 'custom-preset';
setupTestBed({
imports: [ContentTestingModule],
providers: [{ provide: LogService, useValue: { error: jasmine.createSpy('error') } }]
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentMetadataComponent);
component = fixture.componentInstance;
contentMetadataService = TestBed.get(ContentMetadataService);
updateService = TestBed.get(CardViewUpdateService);
nodesApiService = TestBed.get(NodesApiService);
node = <Node> {
id: 'node-id',
aspectNames: [],
nodeType: '',
content: {},
properties: {},
createdByUser: {},
modifiedByUser: {}
};
folderNode = <Node> {
id: 'folder-id',
aspectNames: [],
nodeType: '',
createdByUser: {},
modifiedByUser: {}
};
component.node = node;
component.preset = preset;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
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('Folder', () => {
it('should show the folder node', () => {
component.expanded = false;
fixture.detectChanges();
component.ngOnChanges({ node: new SimpleChange(node, folderNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.directive(CardViewComponent)).componentInstance;
expect(basicPropertiesComponent.properties).toBeDefined();
});
});
});
describe('Saving', () => {
it('should save the node on itemUpdate', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' };
spyOn(nodesApiService, 'updateNode').and.callThrough();
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 = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' };
const expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return of(expectedNode);
});
updateService.update(property, 'updated-value');
fixture.whenStable().then(() => {
expect(component.node).toEqual(expectedNode);
});
}));
it('should throw error on unsuccessful save', () => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' };
const logService: LogService = TestBed.get(LogService);
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return throwError(new Error('My bad'));
});
updateService.update(property, 'updated-value');
expect(logService.error).toHaveBeenCalledWith(new Error('My bad'));
});
it('should raise error message', (done) => {
const property = <CardViewBaseItemModel> { key: 'property-key', value: 'original-value' };
const sub = contentMetadataService.error.subscribe((err) => {
expect(err.statusCode).toBe(0);
expect(err.message).toBe('METADATA.ERRORS.GENERIC');
sub.unsubscribe();
done();
});
spyOn(nodesApiService, 'updateNode').and.callFake(() => {
return throwError(new Error('My bad'));
});
updateService.update(property, 'updated-value');
});
});
describe('Properties loading', () => {
let expectedNode;
beforeEach(() => {
expectedNode = Object.assign({}, node, { name: 'some-modified-value' });
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 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(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 group properties on node change', () => {
spyOn(contentMetadataService, 'getGroupedProperties');
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
expect(contentMetadataService.getGroupedProperties).toHaveBeenCalledWith(expectedNode, 'custom-preset');
});
it('should pass through the loaded group properties to the card view', async(() => {
const expectedProperties = [];
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getGroupedProperties').and.callFake(() => {
return of([{ properties: expectedProperties }]);
});
spyOn(component, 'showGroup').and.returnValue(true);
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const firstGroupedPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container adf-card-view')).componentInstance;
expect(firstGroupedPropertiesComponent.properties).toBe(expectedProperties);
});
}));
it('should pass through the displayEmpty to the card view of grouped properties', async(() => {
component.expanded = true;
component.displayEmpty = false;
fixture.detectChanges();
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] }]));
spyOn(component, 'showGroup').and.returnValue(true);
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesComponent = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container adf-card-view')).componentInstance;
expect(basicPropertiesComponent.displayEmpty).toBe(false);
});
}));
it('should hide card views group when the grouped properties are empty', async(() => {
component.expanded = true;
fixture.detectChanges();
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeNull();
});
}));
it('should display card views group when there is at least one property that is not empty', async(() => {
component.expanded = true;
fixture.detectChanges();
const cardViewGroup = {title: 'Group 1', properties: [{
data: null,
default: null,
displayValue: 'DefaultName',
icon: '',
key: 'properties.cm:default',
label: 'To'
}]};
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of([{ properties: [cardViewGroup] }]));
component.ngOnChanges({ node: new SimpleChange(node, expectedNode, false) });
component.basicProperties$.subscribe(() => {
fixture.detectChanges();
const basicPropertiesGroup = fixture.debugElement.query(By.css('.adf-metadata-grouped-properties-container mat-expansion-panel'));
expect(basicPropertiesGroup).toBeDefined();
});
}));
});
describe('Properties displaying', () => {
it('should hide metadata fields if displayDefaultProperties is set to false', () => {
component.displayDefaultProperties = false;
fixture.detectChanges();
const metadataContainer = fixture.debugElement.query(By.css('mat-expansion-panel[data-automation-id="adf-metadata-group-properties"]'));
fixture.detectChanges();
expect(metadataContainer).toBeNull();
});
it('should display metadata fields if displayDefaultProperties is set to true', () => {
component.displayDefaultProperties = true;
fixture.detectChanges();
const metadataContainer = fixture.debugElement.query(By.css('mat-expansion-panel[data-automation-id="adf-metadata-group-properties"]'));
fixture.detectChanges();
expect(metadataContainer).toBeDefined();
});
it('should have displayDefaultProperties input param as true by default', () => {
expect(component.displayDefaultProperties).toBe(true);
});
});
describe('Expand the panel', () => {
let expectedNode;
beforeEach(() => {
expectedNode = Object.assign({}, node, {name: 'some-modified-value'});
spyOn(contentMetadataService, 'getGroupedProperties').and.returnValue(of(mockGroupProperties));
component.ngOnChanges({node: new SimpleChange(node, expectedNode, false)});
});
it('should open and update drawer with expand section dynamically', async(() => {
component.displayAspect = 'EXIF';
component.expanded = true;
component.displayEmpty = true;
fixture.detectChanges();
let defaultProp = queryDom(fixture);
let exifProp = queryDom(fixture, 'EXIF');
let customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeTruthy();
expect(customProp.componentInstance.expanded).toBeFalsy();
component.displayAspect = 'CUSTOM';
fixture.detectChanges();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeTruthy();
component.displayAspect = 'Properties';
fixture.detectChanges();
defaultProp = queryDom(fixture);
exifProp = queryDom(fixture, 'EXIF');
customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeTruthy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
}));
it('should not expand anything if input is wrong', async(() => {
component.displayAspect = 'XXXX';
component.expanded = true;
component.displayEmpty = true;
fixture.detectChanges();
const defaultProp = queryDom(fixture);
const exifProp = queryDom(fixture, 'EXIF');
const customProp = queryDom(fixture, 'CUSTOM');
expect(defaultProp.componentInstance.expanded).toBeFalsy();
expect(exifProp.componentInstance.expanded).toBeFalsy();
expect(customProp.componentInstance.expanded).toBeFalsy();
}));
});
});
function queryDom(fixture: ComponentFixture<ContentMetadataComponent>, properties: string = 'properties') {
return fixture.debugElement.query(By.css(`[data-automation-id="adf-metadata-group-${properties}"]`));
}

View File

@@ -0,0 +1,176 @@
/*!
* @license
* Copyright 2019 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, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core';
import { Node } from '@alfresco/js-api';
import { Observable, Subject, of } from 'rxjs';
import {
CardViewItem,
NodesApiService,
LogService,
CardViewUpdateService,
AlfrescoApiService,
TranslationService
} from '@alfresco/adf-core';
import { ContentMetadataService } from '../../services/content-metadata.service';
import { CardViewGroup } from '../../interfaces/content-metadata.interfaces';
import { switchMap, takeUntil, catchError } from 'rxjs/operators';
@Component({
selector: 'adf-content-metadata',
templateUrl: './content-metadata.component.html',
styleUrls: ['./content-metadata.component.scss'],
host: { 'class': 'adf-content-metadata' },
encapsulation: ViewEncapsulation.None
})
export class ContentMetadataComponent implements OnChanges, OnInit, OnDestroy {
protected onDestroy$ = new Subject<boolean>();
/** (required) The node entity to fetch metadata about */
@Input()
node: Node;
/** Toggles whether the edit button should be shown */
@Input()
editable: boolean = false;
/** Toggles whether to display empty values in the card view */
@Input()
displayEmpty: boolean = false;
/** Toggles between expanded (ie, full information) and collapsed
* (ie, reduced information) in the display
*/
@Input()
expanded: boolean = false;
/** The multi parameter of the underlying material expansion panel, set to true to allow multi accordion to be expanded at the same time */
@Input()
multi = false;
/** Name of the metadata preset, which defines aspects and their properties */
@Input()
preset: string;
/** Toggles whether the metadata properties should be shown */
@Input()
displayDefaultProperties: boolean = true;
/** (Optional) shows the given aspect in the expanded card */
@Input()
displayAspect: string = null;
basicProperties$: Observable<CardViewItem[]>;
groupedProperties$: Observable<CardViewGroup[]>;
constructor(
private contentMetadataService: ContentMetadataService,
private cardViewUpdateService: CardViewUpdateService,
private nodesApiService: NodesApiService,
private logService: LogService,
private alfrescoApiService: AlfrescoApiService,
private translationService: TranslationService
) {
}
ngOnInit() {
this.cardViewUpdateService.itemUpdated$
.pipe(
switchMap((changes) =>
this.saveNode(changes).pipe(
catchError((err) => {
this.handleUpdateError(err);
return of(null);
})
)
),
takeUntil(this.onDestroy$)
)
.subscribe(
(updatedNode) => {
if (updatedNode) {
Object.assign(this.node, updatedNode);
this.alfrescoApiService.nodeUpdated.next(this.node);
}
}
);
this.loadProperties(this.node);
}
protected handleUpdateError(error: Error) {
this.logService.error(error);
let statusCode = 0;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {
}
let message = `METADATA.ERRORS.${statusCode}`;
if (this.translationService.instant(message) === message) {
message = 'METADATA.ERRORS.GENERIC';
}
this.contentMetadataService.error.next({
statusCode,
message
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.node && !changes.node.firstChange) {
this.loadProperties(changes.node.currentValue);
}
}
private loadProperties(node: Node) {
if (node) {
this.basicProperties$ = this.contentMetadataService.getBasicProperties(node);
this.groupedProperties$ = this.contentMetadataService.getGroupedProperties(node, this.preset);
}
}
private saveNode({ changed: nodeBody }): Observable<Node> {
return this.nodesApiService.updateNode(this.node.id, nodeBody);
}
showGroup(group: CardViewGroup): boolean {
const properties = group.properties.filter((property) => {
return !!property.displayValue;
});
return properties.length > 0;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
public canExpandTheCard(group: CardViewGroup): boolean {
return group.title === this.displayAspect;
}
public canExpandProperties(): boolean {
return !this.expanded || this.displayAspect === 'Properties';
}
}

View File

@@ -0,0 +1,74 @@
/*!
* @license
* Copyright 2019 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 const mockGroupProperties = [
{
'title': 'EXIF',
'properties': [
{
'label': 'Image Width',
'value': 363,
'key': 'properties.exif:pixelXDimension',
'default': null,
'editable': true,
'clickable': false,
'icon': '',
'data': null,
'type': 'int',
'multiline': false,
'pipes': [],
'clickCallBack': null,
'displayValue': 400
},
{
'label': 'Image Height',
'value': 400,
'key': 'properties.exif:pixelYDimension',
'default': null,
'editable': true,
'clickable': false,
'icon': '',
'data': null,
'type': 'int',
'multiline': false,
'pipes': [],
'clickCallBack': null,
'displayValue': 400
}
]
},
{
'title': 'CUSTOM',
'properties': [
{
'label': 'Height',
'value': 400,
'key': 'properties.custom:abc',
'default': null,
'editable': true,
'clickable': false,
'icon': '',
'data': null,
'type': 'int',
'multiline': false,
'pipes': [],
'clickCallBack': null,
'displayValue': 400
}
]
}
];

View File

@@ -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);
}

View File

@@ -0,0 +1,42 @@
/*!
* @license
* Copyright 2019 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 { CoreModule } from '@alfresco/adf-core';
import { ContentMetadataComponent } from './components/content-metadata/content-metadata.component';
import { ContentMetadataCardComponent } from './components/content-metadata-card/content-metadata-card.component';
@NgModule({
imports: [
CommonModule,
MaterialModule,
FlexLayoutModule,
CoreModule
],
exports: [
ContentMetadataComponent,
ContentMetadataCardComponent
],
declarations: [
ContentMetadataComponent,
ContentMetadataCardComponent
]
})
export class ContentMetadataModule {}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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';

View File

@@ -0,0 +1,20 @@
/*!
* @license
* Copyright 2019 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 declare interface AspectOrientedConfig {
[key: string]: string | string[] | boolean;
}

View File

@@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2019 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 CardViewGroup {
title: string;
properties: CardViewItem[];
}

View File

@@ -0,0 +1,27 @@
/*!
* @license
* Copyright 2019 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 { PropertyGroupContainer } from './property-group.interface';
import { OrganisedPropertyGroup } from './organised-property-group.interface';
export interface ContentMetadataConfig {
isGroupAllowed(groupName: string): boolean;
reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[];
filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[];
appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[];
isIncludeAllEnabled(): boolean;
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2019 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-oriented-config.interface';
export * from './property.interface';
export * from './property-group.interface';
export * from './organised-property-group.interface';
export * from './card-view-group.interface';
export * from './content-metadata-config.interface';
export * from './indifferent-config.interface';
export * from './layout-oriented-config.interface';
export * from './preset-config.interface';

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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 declare type InDifferentConfig = '*';

View File

@@ -0,0 +1,31 @@
/*!
* @license
* Copyright 2019 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 LayoutOrientedConfigItem {
aspect?: string;
type?: string;
properties: string | string[];
includeAll?: boolean;
exclude?: string | string[];
}
export interface LayoutOrientedConfigLayoutBlock {
title: string;
items: LayoutOrientedConfigItem[];
}
export type LayoutOrientedConfig = Array<LayoutOrientedConfigLayoutBlock>;

View File

@@ -0,0 +1,24 @@
/*!
* @license
* Copyright 2019 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 { Property } from './property.interface';
export interface OrganisedPropertyGroup {
title: string;
name?: string;
properties: Property[];
}

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 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 { InDifferentConfig } from './indifferent-config.interface';
import { AspectOrientedConfig } from './aspect-oriented-config.interface';
import { LayoutOrientedConfig } from './layout-oriented-config.interface';
export declare type PresetConfig = InDifferentConfig | AspectOrientedConfig | LayoutOrientedConfig;

View File

@@ -0,0 +1,31 @@
/*!
* @license
* Copyright 2019 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 { Property } from './property.interface';
export interface PropertyGroup {
name: string;
title: string;
description?: string;
properties: {
[key: string]: Property
};
}
export interface PropertyGroupContainer {
[key: string]: PropertyGroup;
}

View File

@@ -0,0 +1,26 @@
/*!
* @license
* Copyright 2019 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 Property {
name: string;
title: string;
description?: string;
dataType: string;
defaultValue?: any;
mandatory: boolean;
multiValued: boolean;
}

View File

@@ -0,0 +1,29 @@
/*!
* @license
* Copyright 2019 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 './components/content-metadata-card/content-metadata-card.component';
export * from './services/basic-properties.service';
export * from './services/content-metadata.service';
export * from './services/property-descriptors.service';
export * from './services/property-groups-translator.service';
export * from './services/config/content-metadata-config.factory';
export * from './services/config/indifferent-config.service';
export * from './services/config/layout-oriented-config.service';
export * from './services/config/aspect-oriented-config.service';
export * from './content-metadata.module';

View File

@@ -0,0 +1,105 @@
/*!
* @license
* Copyright 2019 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 { Node } from '@alfresco/js-api';
import { CardViewDateItemModel, CardViewTextItemModel, FileSizePipe } from '@alfresco/adf-core';
@Injectable({
providedIn: 'root'
})
export class BasicPropertiesService {
constructor(private fileSizePipe: FileSizePipe) {
}
getProperties(node: Node) {
const sizeInBytes = node.content ? node.content.sizeInBytes : '',
mimeTypeName = node.content ? node.content.mimeTypeName : '',
author = node.properties ? node.properties['cm:author'] : '',
description = node.properties ? node.properties['cm:description'] : '',
title = node.properties ? node.properties['cm:title'] : '';
return [
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.NAME',
value: node.name,
key: 'name',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.TITLE',
value: title,
key: 'properties.cm:title',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.CREATOR',
value: node.createdByUser.displayName,
key: 'createdByUser.displayName',
editable: false
}),
new CardViewDateItemModel({
label: 'CORE.METADATA.BASIC.CREATED_DATE',
value: node.createdAt,
key: 'createdAt',
editable: false,
format: 'mediumDate'
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.SIZE',
value: sizeInBytes,
key: 'content.sizeInBytes',
pipes: [{ pipe: this.fileSizePipe }],
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.MODIFIER',
value: node.modifiedByUser.displayName,
key: 'modifiedByUser.displayName',
editable: false
}),
new CardViewDateItemModel({
label: 'CORE.METADATA.BASIC.MODIFIED_DATE',
value: node.modifiedAt,
key: 'modifiedAt',
editable: false,
format: 'mediumDate'
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.MIMETYPE',
value: mimeTypeName,
key: 'content.mimeTypeName',
editable: false
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.AUTHOR',
value: author,
key: 'properties.cm:author',
editable: true
}),
new CardViewTextItemModel({
label: 'CORE.METADATA.BASIC.DESCRIPTION',
value: description,
key: 'properties.cm:description',
multiline: true,
editable: true
})
];
}
}

View File

@@ -0,0 +1,226 @@
/*!
* @license
* Copyright 2019 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 { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { AspectOrientedConfig, Property, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
describe('AspectOrientedConfigService', () => {
let configService: AspectOrientedConfigService;
function createConfigService(configObj: AspectOrientedConfig) {
return new AspectOrientedConfigService(configObj);
}
describe('reorganiseByConfig', () => {
interface TestCase {
name: string;
config: AspectOrientedConfig;
expectations: OrganisedPropertyGroup[];
}
const property1 = <Property> { name: 'property1' },
property2 = <Property> { name: 'property2' },
property3 = <Property> { name: 'property3' },
property4 = <Property> { name: 'property4' };
const propertyGroups: PropertyGroupContainer = {
berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } },
zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }
};
const testCases: TestCase[] = [
{
name: 'Empty config',
config: {},
expectations: []
},
{
name: 'One property from One group',
config: {
'berseria': [ 'property1' ]
},
expectations: [{
title: 'Berseria',
properties: [ property1 ]
}]
},
{
name: 'More properties from One group',
config: {
'berseria': [ 'property1', 'property2' ]
},
expectations: [{
title: 'Berseria',
properties: [ property1, property2 ]
}]
},
{
name: 'One-one properties from More group',
config: {
'berseria': [ 'property1' ],
'zestiria': [ 'property3' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1 ]
},
{
title: 'Zestiria',
properties: [ property3 ]
}
]
},
{
name: 'More properties from More groups',
config: {
'zestiria': [ 'property4', 'property3' ],
'berseria': [ 'property2', 'property1' ]
},
expectations: [
{
title: 'Zestiria',
properties: [ property4, property3 ]
},
{
title: 'Berseria',
properties: [ property2, property1 ]
}
]
},
{
name: 'Wildcard',
config: {
'berseria': '*',
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property4 ]
}
]
},
{
name: 'Not existing group',
config: {
'berseria': '*',
'not-existing-group': '*',
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property4 ]
}
]
},
{
name: 'Not existing property',
config: {
'berseria': [ 'not-existing-property' ],
'zestiria': [ 'property4' ]
},
expectations: [
{
title: 'Zestiria',
properties: [ property4 ]
}
]
}
];
testCases.forEach((testCase) => {
it(`should pass for: ${testCase.name}`, () => {
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.reorganiseByConfig(propertyGroups);
expect(organisedPropertyGroups.length).toBe(testCase.expectations.length, 'Group count should match');
testCase.expectations.forEach((expectation, i) => {
expect(organisedPropertyGroups[i].title).toBe(expectation.title, 'Group\'s title should match' );
expect(organisedPropertyGroups[i].properties.length).toBe(
expectation.properties.length,
`Property count for "${organisedPropertyGroups[i].title}" group should match.`
);
expectation.properties.forEach((property, j) => {
expect(organisedPropertyGroups[i].properties[j]).toBe(property, `Property should match ${property.name}`);
});
});
});
});
});
describe('appendAllPreset', () => {
const property1 = <Property> { name: 'property1' },
property2 = <Property> { name: 'property2' },
property3 = <Property> { name: 'property3' },
property4 = <Property> { name: 'property4' };
const propertyGroups: PropertyGroupContainer = {
berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } },
zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }
};
it(`should return all the propertyGorups`, () => {
const testCase = {
name: 'Not existing property',
config: {
includeAll: true
},
expectations: [
{
title: 'Berseria',
properties: [ property1, property2 ]
},
{
title: 'Zestiria',
properties: [ property3, property4 ]
}
]
};
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.appendAllPreset(propertyGroups);
expect(organisedPropertyGroups.length).toBe(testCase.expectations.length, 'Group count should match');
testCase.expectations.forEach((expectation, i) => {
expect(organisedPropertyGroups[i].title).toBe(expectation.title, 'Group\'s title should match' );
expect(organisedPropertyGroups[i].properties.length).toBe(
expectation.properties.length,
`Property count for "${organisedPropertyGroups[i].title}" group should match.`
);
expectation.properties.forEach((property, j) => {
expect(organisedPropertyGroups[i].properties[j]).toBe(property, `Property should match ${property.name}`);
});
});
});
});
});

View File

@@ -0,0 +1,93 @@
/*!
* @license
* Copyright 2019 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 { ContentMetadataConfig, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
import { getGroup, getProperty } from './property-group-reader';
export class AspectOrientedConfigService implements ContentMetadataConfig {
constructor(private config: any) { }
public isGroupAllowed(groupName: string): boolean {
if (this.isIncludeAllEnabled()) {
return true;
}
const groupNames = Object.keys(this.config);
return groupNames.indexOf(groupName) !== -1;
}
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const aspects = this.config,
aspectNames = Object.keys(aspects);
return aspectNames
.reduce((groupAccumulator, aspectName) => {
const newGroup = this.getOrganisedPropertyGroup(propertyGroups, aspectName);
return groupAccumulator.concat(newGroup);
}, [])
.filter((organisedPropertyGroup) => organisedPropertyGroup.properties.length > 0);
}
public appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const groups = Object.keys(propertyGroups)
.map((groupName) => {
const propertyGroup = propertyGroups[groupName],
properties = propertyGroup.properties;
return Object.assign({}, propertyGroup, {
properties: Object.keys(properties).map((propertyName) => properties[propertyName])
});
});
return groups;
}
public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
if (this.config.exclude) {
return propertyGroups.filter((preset) => {
return !this.config.exclude.includes(preset.name);
});
}
return propertyGroups;
}
public isIncludeAllEnabled() {
return this.config.includeAll;
}
private getOrganisedPropertyGroup(propertyGroups, aspectName) {
const group = getGroup(propertyGroups, aspectName);
let newGroup = [];
if (group) {
const aspectProperties = this.config[aspectName];
let properties;
if (aspectProperties === '*') {
properties = getProperty(propertyGroups, aspectName, aspectProperties);
} else {
properties = (<string[]> aspectProperties)
.map((propertyName) => getProperty(propertyGroups, aspectName, propertyName))
.filter((props) => props !== undefined);
}
newGroup = [{ title: group.title, properties }];
}
return newGroup;
}
}

View File

@@ -0,0 +1,131 @@
/*!
* @license
* Copyright 2019 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, LogService, setupTestBed } from '@alfresco/adf-core';
import { IndifferentConfigService } from './indifferent-config.service';
import { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { LayoutOrientedConfigService } from './layout-oriented-config.service';
import { ContentMetadataConfigFactory } from './content-metadata-config.factory';
import { ContentMetadataConfig } from '../../interfaces/content-metadata.interfaces';
import { HttpClientModule } from '@angular/common/http';
describe('ContentMetadataConfigFactory', () => {
let factory: ContentMetadataConfigFactory;
let appConfig: AppConfigService;
let config: ContentMetadataConfig;
setupTestBed({
imports: [
HttpClientModule
],
providers: [
ContentMetadataConfigFactory,
AppConfigService,
{
provide: LogService, useValue: {
error: () => {
}
}
}
]
});
beforeEach(async(() => {
factory = TestBed.get(ContentMetadataConfigFactory);
appConfig = TestBed.get(AppConfigService);
}));
describe('get', () => {
let logService;
beforeEach(async(() => {
logService = TestBed.get(LogService);
spyOn(logService, 'error').and.stub();
}));
afterEach(() => {
TestBed.resetTestingModule();
});
describe('get', () => {
it('should get back to default preset if no preset is provided as parameter', async(() => {
config = factory.get();
expect(config).toEqual(jasmine.any(IndifferentConfigService));
}));
it('should get back to default preset if no preset is set', async(() => {
config = factory.get('default');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
expect(logService.error).not.toHaveBeenCalled();
}));
it('should get back to the default preset if the requested preset does not exist', async(() => {
config = factory.get('not-existing-preset');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
}));
it('should log an error message if the requested preset does not exist', async(() => {
config = factory.get('not-existing-preset');
expect(logService.error).toHaveBeenCalledWith('No content-metadata preset for: not-existing-preset');
}));
});
describe('set', () => {
function setConfig(presetName, presetConfig) {
appConfig.config['content-metadata'] = {
presets: {
[presetName]: presetConfig
}
};
}
it('should get back the IndifferentConfigService preset if the preset config is indifferent', async(() => {
setConfig('default', '*');
config = factory.get('default');
expect(config).toEqual(jasmine.any(IndifferentConfigService));
}));
it('should get back the AspectOrientedConfigService preset if the preset config is aspect oriented', async(() => {
setConfig('default', { 'exif:exif': '*' });
config = factory.get('default');
expect(config).toEqual(jasmine.any(AspectOrientedConfigService));
}));
it('should get back the LayoutOrientedConfigService preset if the preset config is layout oriented', async(() => {
setConfig('default', []);
config = factory.get('default');
expect(config).toEqual(jasmine.any(LayoutOrientedConfigService));
}));
});
});
});

View File

@@ -0,0 +1,80 @@
/*!
* @license
* Copyright 2019 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';
import { AspectOrientedConfigService } from './aspect-oriented-config.service';
import { IndifferentConfigService } from './indifferent-config.service';
import { LayoutOrientedConfigService } from './layout-oriented-config.service';
import {
PresetConfig,
ContentMetadataConfig,
AspectOrientedConfig,
LayoutOrientedConfig
} from '../../interfaces/content-metadata.interfaces';
@Injectable({
providedIn: 'root'
})
export class ContentMetadataConfigFactory {
static readonly INDIFFERENT_PRESET = '*';
static readonly DEFAULT_PRESET_NAME = 'default';
constructor(private appConfigService: AppConfigService, private logService: LogService) {}
public get(presetName: string = 'default'): ContentMetadataConfig {
let presetConfig: PresetConfig;
try {
presetConfig = this.appConfigService.config['content-metadata'].presets[presetName];
} catch {
if (presetName !== ContentMetadataConfigFactory.DEFAULT_PRESET_NAME) {
this.logService.error(`No content-metadata preset for: ${presetName}`);
}
presetConfig = ContentMetadataConfigFactory.INDIFFERENT_PRESET;
}
return this.createConfig(presetConfig);
}
private createConfig(presetConfig: PresetConfig): ContentMetadataConfig {
let config: ContentMetadataConfig;
if (this.isLayoutOrientedPreset(presetConfig)) {
config = new LayoutOrientedConfigService(<LayoutOrientedConfig> presetConfig);
} else if (this.isAspectOrientedPreset(presetConfig)) {
config = new AspectOrientedConfigService(<AspectOrientedConfig> presetConfig);
} else {
config = new IndifferentConfigService();
}
Object.freeze(config);
return config;
}
private isAspectOrientedPreset(presetConfig: PresetConfig): boolean {
return this.isObject(presetConfig);
}
private isLayoutOrientedPreset(presetConfig: PresetConfig): boolean {
return Array.isArray(presetConfig);
}
private isObject(x: any): boolean {
return x != null && typeof x === 'object';
}
}

View File

@@ -0,0 +1,51 @@
/*!
* @license
* Copyright 2019 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 { ContentMetadataConfig, OrganisedPropertyGroup,
PropertyGroupContainer
} from '../../interfaces/content-metadata.interfaces';
export class IndifferentConfigService implements ContentMetadataConfig {
isGroupAllowed(): boolean {
return true;
}
reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
return Object.keys(propertyGroups)
.map((groupName) => {
const propertyGroup = propertyGroups[groupName],
properties = propertyGroup.properties;
return Object.assign({}, propertyGroup, {
properties: Object.keys(properties).map((propertyName) => properties[propertyName])
});
});
}
filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
return propertyGroups;
}
appendAllPreset(): OrganisedPropertyGroup[] {
return[];
}
isIncludeAllEnabled(): boolean {
return true;
}
}

View File

@@ -0,0 +1,254 @@
/*!
* @license
* Copyright 2019 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 { LayoutOrientedConfigService } from './layout-oriented-config.service';
import { LayoutOrientedConfig, Property, OrganisedPropertyGroup, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
describe('LayoutOrientedConfigService', () => {
let configService: LayoutOrientedConfigService;
function createConfigService(configObj: LayoutOrientedConfig) {
return new LayoutOrientedConfigService(configObj);
}
describe('isGroupAllowed', () => {
const testCases = [
{
config: [],
expectation: false,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [{ aspect: 'berseria', properties: '*' }] }],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [{ type: 'berseria', properties: '*' }] }],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [{ title: 'Deamons', items: [
{ aspect: 'zestiria', properties: '*' }, { aspect: 'berseria', properties: '*' }
]}],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [
{ title: 'Deamons', items: [{ aspect: 'zestiria', properties: '*' }] },
{ title: 'Malakhims', items: [{ aspect: 'berseria', properties: '*' }] }
],
expectation: true,
groupNameToQuery: 'berseria'
},
{
config: [
{ title: 'Deamons', items: [{ aspect: 'zestiria', properties: '*' }] },
{ title: 'Malakhims', items: [{ type: 'berseria', properties: '*' }] }
],
expectation: false,
groupNameToQuery: 'phantasia'
},
{
config: [
{ title: 'Deamons', includeAll: true, items: [{ aspect: 'zestiria', properties: '*' }] }
],
expectation: true,
groupNameToQuery: 'phantasia'
}
];
testCases.forEach((testCase, index) => {
it(`should return ${testCase.expectation.toString()} for test case index #${index}`, () => {
configService = createConfigService(testCase.config);
const isAllowed = configService.isGroupAllowed(testCase.groupNameToQuery);
expect(isAllowed).toBe(testCase.expectation);
});
});
});
describe('reorganiseByConfig', () => {
interface TestCase {
name: string;
config: LayoutOrientedConfig;
expectations: OrganisedPropertyGroup[];
}
const property1 = <Property> { name: 'property1' },
property2 = <Property> { name: 'property2' },
property3 = <Property> { name: 'property3' },
property4 = <Property> { name: 'property4' };
const propertyGroups: PropertyGroupContainer = {
berseria: { title: 'Berseria', description: '', name: 'berseria', properties: { property1, property2 } },
zestiria: { title: 'Zestiria', description: '', name: 'zestiria', properties: { property3, property4 } }
};
const testCases: TestCase[] = [
{
name: 'Empty config',
config: [],
expectations: []
},
{
name: 'First property of a group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property1 ] }
]
},
{
name: 'Second property of a group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property2' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property2 ] }
]
},
{
name: 'More properties from one group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'berseria', properties: [ 'property2', 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property2, property1 ] }
]
},
{
name: 'First property of the second group in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4 ] }
]
},
{
name: 'One-one properties from multiple groups in one item',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] },
{ aspect: 'berseria', properties: [ 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4, property1 ] }
]
},
{
name: 'Multiple properties mixed from multiple groups in multiple items',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: [ 'property4' ] },
{ type: 'berseria', properties: [ 'property1' ] }
]},
{ title: 'Second group', items: [
{ aspect: 'zestiria', properties: [ 'property3' ] },
{ type: 'berseria', properties: [ 'property2', 'property1' ] },
{ aspect: 'zestiria', properties: [ 'property4' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property4, property1 ] },
{ title: 'Second group', properties: [ property3, property2, property1, property4 ] }
]
},
{
name: 'Multiple properties mixed from multiple groups in multiple items with "*"',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'berseria', properties: [ 'property1' ] }
]},
{ title: 'Second group', items: [
{ type: 'berseria', properties: [ 'property2', 'property1' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property1 ] },
{ title: 'Second group', properties: [ property2, property1 ] }
]
},
{
name: 'Not existing property',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'berseria', properties: [ 'not-existing-property' ] },
{ type: 'berseria', properties: [ 'property2' ] }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property2 ] }
]
},
{
name: 'Not existing group',
config: [
{ title: 'First group', items: [
{ aspect: 'zestiria', properties: '*' },
{ type: 'not-existing-group', properties: '*' },
{ type: 'berseria', properties: [ 'property2' ] },
{ type: 'not-existing-group', properties: 'not-existing-property' }
]}
],
expectations: [
{ title: 'First group', properties: [ property3, property4, property2 ] }
]
}
];
testCases.forEach((testCase) => {
it(`should pass for: ${testCase.name}`, () => {
configService = createConfigService(testCase.config);
const organisedPropertyGroups = configService.reorganiseByConfig(propertyGroups);
expect(organisedPropertyGroups.length).toBe(testCase.expectations.length, 'Group count should match');
testCase.expectations.forEach((expectation, i) => {
expect(organisedPropertyGroups[i].title).toBe(expectation.title, 'Group\'s title should match' );
expect(organisedPropertyGroups[i].properties.length).toBe(
expectation.properties.length,
`Property count for "${organisedPropertyGroups[i].title}" group should match.`
);
expectation.properties.forEach((property, j) => {
expect(organisedPropertyGroups[i].properties[j]).toBe(property, `Property should match ${property.name}`);
});
});
});
});
});
});

View File

@@ -0,0 +1,112 @@
/*!
* @license
* Copyright 2019 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 {
ContentMetadataConfig,
LayoutOrientedConfigItem,
OrganisedPropertyGroup,
PropertyGroupContainer
} from '../../interfaces/content-metadata.interfaces';
import { getProperty } from './property-group-reader';
export class LayoutOrientedConfigService implements ContentMetadataConfig {
constructor(private config: any) { }
public isGroupAllowed(groupName: string): boolean {
if (this.isIncludeAllEnabled()) {
return true;
}
return this.getMatchingGroups(groupName).length > 0;
}
public reorganiseByConfig(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
const layoutBlocks = this.config.filter((itemsGroup) => itemsGroup.items);
const organisedPropertyGroup = layoutBlocks.map((layoutBlock) => {
const flattenedItems = this.flattenItems(layoutBlock.items),
properties = flattenedItems.reduce((props, explodedItem) => {
const property = getProperty(propertyGroups, explodedItem.groupName, explodedItem.propertyName) || [];
return props.concat(property);
}, []);
return {
title: layoutBlock.title,
properties
};
});
return organisedPropertyGroup;
}
public appendAllPreset(propertyGroups: PropertyGroupContainer): OrganisedPropertyGroup[] {
return Object.keys(propertyGroups)
.map((groupName) => {
const propertyGroup = propertyGroups[groupName],
properties = propertyGroup.properties;
return Object.assign({}, propertyGroup, {
properties: Object.keys(properties).map((propertyName) => properties[propertyName])
});
});
}
public filterExcludedPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
let excludedConfig = this.config
.map((config) => config.exclude)
.find((exclude) => exclude !== undefined);
if (excludedConfig === undefined) {
excludedConfig = [];
} else if (typeof excludedConfig === 'string') {
excludedConfig = [excludedConfig];
}
return propertyGroups.filter((props) => {
return !excludedConfig.includes(props.name);
});
}
public isIncludeAllEnabled() {
const includeAllProperty = this.config
.map((config) => config.includeAll)
.find((includeAll) => includeAll !== undefined);
return includeAllProperty !== undefined ? includeAllProperty : false;
}
private flattenItems(items) {
return items.reduce((accumulator, item) => {
const properties = Array.isArray(item.properties) ? item.properties : [item.properties];
const flattenedProperties = properties.map((propertyName) => {
return {
groupName: item.aspect || item.type,
propertyName
};
});
return accumulator.concat(flattenedProperties);
}, []);
}
private getMatchingGroups(groupName: string): LayoutOrientedConfigItem[] {
return this.config
.map((layoutBlock) => layoutBlock.items)
.reduce((accumulator, items) => accumulator.concat(items), [])
.filter((item) => item.aspect === groupName || item.type === groupName);
}
}

View File

@@ -0,0 +1,43 @@
/*!
* @license
* Copyright 2019 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 { PropertyGroup, Property, PropertyGroupContainer } from '../../interfaces/content-metadata.interfaces';
const emptyGroup = {
properties: {}
};
function convertObjectToArray(object: any): Property[] {
return Object.keys(object).map((key) => object[key]);
}
export function getGroup(propertyGroups: PropertyGroupContainer, groupName: string): PropertyGroup | undefined {
return propertyGroups[groupName];
}
export function getProperty(propertyGroups: PropertyGroupContainer, groupName: string, propertyName: string): Property | Property[] | undefined {
const groupDefinition = getGroup(propertyGroups, groupName) || emptyGroup;
let propertyDefinitions;
if (propertyName === '*') {
propertyDefinitions = convertObjectToArray(groupDefinition.properties);
} else {
propertyDefinitions = groupDefinition.properties[propertyName];
}
return propertyDefinitions;
}

View File

@@ -0,0 +1,249 @@
/*!
* @license
* Copyright 2019 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 { AlfrescoApiService, AppConfigService, setupTestBed } from '@alfresco/adf-core';
import { ClassesApi, Node } from '@alfresco/js-api';
import { TestBed } from '@angular/core/testing';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { ContentMetadataService } from './content-metadata.service';
import { of } from 'rxjs';
import { PropertyGroup } from '../interfaces/property-group.interface';
describe('ContentMetaDataService', () => {
let service: ContentMetadataService;
let classesApi: ClassesApi;
let appConfig: AppConfigService;
const exifResponse: PropertyGroup = {
name: 'exif:exif',
title: 'Exif',
properties: {
'exif:1': { title: 'exif:1:id', name: 'exif:1', dataType: '', mandatory: false, multiValued: false },
'exif:2': { title: 'exif:2:id', name: 'exif:2', dataType: '', mandatory: false, multiValued: false }
}
};
const contentResponse: PropertyGroup = {
name: 'cm:content',
title: '',
properties: {
'cm:content': { title: 'cm:content:id', name: 'cm:content', dataType: '', mandatory: false, multiValued: false }
}
};
setupTestBed({
imports: [ContentTestingModule]
});
function setConfig(presetName, presetConfig) {
appConfig.config['content-metadata'] = {
presets: {
[presetName]: presetConfig
}
};
}
beforeEach(() => {
service = TestBed.get(ContentMetadataService);
const alfrescoApiService = TestBed.get(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
appConfig = TestBed.get(AppConfigService);
});
it('should return all the properties of the node', () => {
const fakeNode: Node = <Node> {
name: 'Node',
id: 'fake-id',
isFile: true,
aspectNames: ['exif:exif'],
createdByUser: {displayName: 'test-user'},
modifiedByUser: {displayName: 'test-user-modified'}
};
service.getBasicProperties(fakeNode).subscribe(
(res) => {
expect(res.length).toEqual(10);
expect(res[0].value).toEqual('Node');
expect(res[1].value).toBeFalsy();
expect(res[2].value).toBe('test-user');
}
);
});
describe('AspectOriented preset', () => {
it('should return response with exif property', (done) => {
const fakeNode: Node = <Node> { name: 'Node', id: 'fake-id', isFile: true, aspectNames: ['exif:exif'] } ;
setConfig('default', { 'exif:exif': '*' });
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(exifResponse);
});
service.getGroupedProperties(fakeNode).subscribe(
(res) => {
expect(res.length).toEqual(1);
expect(res[0].title).toEqual('Exif');
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('exif_exif');
});
it('should filter the record options for node ', (done) => {
const fakeNode: Node = <Node> { name: 'Node', id: 'fake-id', isFile: true, aspectNames: ['exif:exif'] } ;
setConfig('default', { 'exif:exif': '*', 'rma:record': '*' });
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(exifResponse);
});
service.getGroupedProperties(fakeNode).subscribe(
(res) => {
expect(res.length).toEqual(1);
expect(res[0].title).toEqual('Exif');
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('exif_exif');
});
});
describe('LayoutOriented preset', () => {
it('should return the node property', (done) => {
const fakeNode: Node = <Node> { name: 'Node Action', id: 'fake-id', nodeType: 'cm:content', isFile: true, aspectNames: [] } ;
const customLayoutOrientedScheme = [
{
'id': 'app.content.metadata.customGroup2',
'title': 'Properties',
'items': [
{
'id': 'app.content.metadata.content',
'aspect': 'cm:content',
'properties': '*'
}
]
}
];
setConfig('custom', customLayoutOrientedScheme);
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(contentResponse);
});
service.getGroupedProperties(fakeNode, 'custom').subscribe(
(res) => {
expect(res.length).toEqual(1);
expect(res[0].title).toEqual('Properties');
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
});
it('should filter the exif property', (done) => {
const fakeNode: Node = <Node> { name: 'Node Action', id: 'fake-id', nodeType: 'cm:content', isFile: true, aspectNames: [] } ;
const customLayoutOrientedScheme = [
{
'id': 'app.content.metadata.customGroup',
'title': 'Exif',
'items': [
{
'id': 'app.content.metadata.exifAspect2',
'aspect': 'exif:exif',
'properties': '*'
}
]
},
{
'id': 'app.content.metadata.customGroup2',
'title': 'Properties',
'items': [
{
'id': 'app.content.metadata.content',
'aspect': 'cm:content',
'properties': '*'
}
]
}
];
setConfig('custom', customLayoutOrientedScheme);
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(contentResponse);
});
service.getGroupedProperties(fakeNode, 'custom').subscribe(
(res) => {
expect(res.length).toEqual(1);
expect(res[0].title).toEqual('Properties');
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
});
it('should exclude the property if this property is excluded from config', (done) => {
const fakeNode: Node = <Node> { name: 'Node Action', id: 'fake-id', nodeType: 'cm:content', isFile: true, aspectNames: [] } ;
const customLayoutOrientedScheme = [
{
'id': 'app.content.metadata.customGroup',
'title': 'Exif',
'includeAll': true,
'exclude': ['cm:content'],
'items': [
{
'id': 'app.content.metadata.exifAspect2',
'aspect': 'exif:exif',
'properties': '*'
}
]
}
];
setConfig('custom', customLayoutOrientedScheme);
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(contentResponse);
});
service.getGroupedProperties(fakeNode, 'custom').subscribe(
(res) => {
expect(res.length).toEqual(0);
done();
}
);
expect(classesApi.getClass).toHaveBeenCalledTimes(1);
expect(classesApi.getClass).toHaveBeenCalledWith('cm_content');
});
});
});

View File

@@ -0,0 +1,84 @@
/*!
* @license
* Copyright 2019 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 { Node } from '@alfresco/js-api';
import { BasicPropertiesService } from './basic-properties.service';
import { Observable, of, iif, Subject } from 'rxjs';
import { PropertyGroupTranslatorService } from './property-groups-translator.service';
import { CardViewItem } from '@alfresco/adf-core';
import { CardViewGroup, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import { ContentMetadataConfigFactory } from './config/content-metadata-config.factory';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { map, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ContentMetadataService {
error = new Subject<{ statusCode: number, message: string }>();
constructor(private basicPropertiesService: BasicPropertiesService,
private contentMetadataConfigFactory: ContentMetadataConfigFactory,
private propertyGroupTranslatorService: PropertyGroupTranslatorService,
private propertyDescriptorsService: PropertyDescriptorsService) {
}
getBasicProperties(node: Node): Observable<CardViewItem[]> {
return of(this.basicPropertiesService.getProperties(node));
}
getGroupedProperties(node: Node, presetName: string = 'default'): Observable<CardViewGroup[]> {
let groupedProperties = of([]);
if (node.aspectNames) {
const contentMetadataConfig = this.contentMetadataConfigFactory.get(presetName),
groupNames = node.aspectNames
.concat(node.nodeType)
.filter((groupName) => contentMetadataConfig.isGroupAllowed(groupName));
if (groupNames.length > 0) {
groupedProperties = this.propertyDescriptorsService.load(groupNames).pipe(
switchMap((groups) =>
iif(
() => contentMetadataConfig.isIncludeAllEnabled(),
of(contentMetadataConfig.appendAllPreset(groups).concat(contentMetadataConfig.reorganiseByConfig(groups))),
of(contentMetadataConfig.reorganiseByConfig(groups))
)),
map((groups) => contentMetadataConfig.filterExcludedPreset(groups)),
map((groups) => this.filterEmptyPreset(groups)),
map((groups) => this.setTitleToNameIfNotSet(groups)),
map((groups) => this.propertyGroupTranslatorService.translateToCardViewGroups(groups, node.properties))
);
}
}
return groupedProperties;
}
setTitleToNameIfNotSet(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
propertyGroups.map((propertyGroup) => {
propertyGroup.title = propertyGroup.title || propertyGroup.name;
});
return propertyGroups;
}
filterEmptyPreset(propertyGroups: OrganisedPropertyGroup[]): OrganisedPropertyGroup[] {
return propertyGroups.filter((props) => props.properties.length);
}
}

View File

@@ -0,0 +1,88 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { PropertyDescriptorsService } from './property-descriptors.service';
import { AlfrescoApiService, setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ClassesApi } from '@alfresco/js-api';
import { PropertyGroup } from '../interfaces/content-metadata.interfaces';
import { ContentTestingModule } from '../../testing/content.testing.module';
describe('PropertyDescriptorLoaderService', () => {
let service: PropertyDescriptorsService;
let classesApi: ClassesApi;
setupTestBed({
imports: [ContentTestingModule]
});
beforeEach(() => {
service = TestBed.get(PropertyDescriptorsService);
const alfrescoApiService = TestBed.get(AlfrescoApiService);
classesApi = alfrescoApiService.classesApi;
});
it('should load the groups passed by paramter', () => {
spyOn(classesApi, 'getClass');
service.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: PropertyGroup = {
name: 'exif:exif',
title: '',
properties: {
'exif:1': { title: 'exif:1:id', name: 'exif:1', dataType: '', mandatory: false, multiValued: false },
'exif:2': { title: 'exif:2:id', name: 'exif:2', dataType: '', mandatory: false, multiValued: false }
}
};
const contentResponse: PropertyGroup = {
name: 'cm:content',
title: '',
properties: {
'cm:content': { title: 'cm:content:id', name: 'cm:content', dataType: '', mandatory: false, multiValued: false }
}
};
const apiResponses = [ exifResponse, contentResponse ];
let counter = 0;
spyOn(classesApi, 'getClass').and.callFake(() => {
return of(apiResponses[counter++]);
});
service.load(['exif:exif', 'cm:content'])
.subscribe({
next: (data) => {
expect(data['exif:exif']).toBe(exifResponse);
expect(data['cm:content']).toBe(contentResponse);
},
complete: done
});
});
});

View File

@@ -0,0 +1,48 @@
/*!
* @license
* Copyright 2019 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 { Observable, defer, forkJoin } from 'rxjs';
import { PropertyGroup, PropertyGroupContainer } from '../interfaces/content-metadata.interfaces';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class PropertyDescriptorsService {
constructor(private alfrescoApiService: AlfrescoApiService) {}
load(groupNames: string[]): Observable<PropertyGroupContainer> {
const groupFetchStreams = groupNames
.map((groupName) => groupName.replace(':', '_'))
.map((groupName) => defer( () => this.alfrescoApiService.classesApi.getClass(groupName)) );
return forkJoin(groupFetchStreams).pipe(
map(this.convertToObject)
);
}
private convertToObject(propertyGroupsArray: PropertyGroup[]): PropertyGroupContainer {
return propertyGroupsArray.reduce((propertyGroups, propertyGroup) => {
return Object.assign({}, propertyGroups, {
[propertyGroup.name]: propertyGroup
});
}, {});
}
}

View File

@@ -0,0 +1,300 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { PropertyGroupTranslatorService } from './property-groups-translator.service';
import { Property, OrganisedPropertyGroup } from '../interfaces/content-metadata.interfaces';
import {
CardViewTextItemModel,
CardViewDateItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
CardViewBoolItemModel,
CardViewDatetimeItemModel,
setupTestBed
} from '@alfresco/adf-core';
import { ContentTestingModule } from '../../testing/content.testing.module';
describe('PropertyGroupTranslatorService', () => {
let service: PropertyGroupTranslatorService;
let propertyGroups: OrganisedPropertyGroup[];
let propertyGroup: OrganisedPropertyGroup;
let property: Property;
let propertyValues: { [key: string]: any };
setupTestBed({
imports: [ContentTestingModule],
providers: [
{
provide: LogService, useValue: {
error: () => {
}
}
}
]
});
beforeEach(() => {
service = TestBed.get(PropertyGroupTranslatorService);
property = {
name: 'FAS:PLAGUE',
title: 'The Faro Plague',
dataType: '',
defaultValue: '',
mandatory: false,
multiValued: false
};
propertyGroup = {
title: 'Faro Automated Solutions',
properties: [property]
};
propertyGroups = [];
});
afterEach(() => {
TestBed.resetTestingModule();
});
describe('General transformation', () => {
it('should translate EVERY properties in ONE group properly', () => {
propertyGroup.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
}];
propertyGroups.push(propertyGroup);
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(cardViewGroup[0].properties.length).toBe(2);
expect(cardViewGroup[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First property should be instance of CardViewTextItemModel');
expect(cardViewGroup[0].properties[1] instanceof CardViewTextItemModel).toBeTruthy('Second property should be instance of CardViewTextItemModel');
});
it('should translate EVERY property in EVERY group properly', () => {
propertyGroups.push(
Object.assign({}, propertyGroup, {
properties: [{
name: 'FAS:PLAGUE',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
}),
Object.assign({}, propertyGroup, {
properties: [{
name: 'FAS:ALOY',
title: 'title',
dataType: 'd:text',
defaultValue: 'defaultvalue',
mandatory: false,
multiValued: false
}]
})
);
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(cardViewGroup.length).toBe(2);
expect(cardViewGroup[0].properties[0] instanceof CardViewTextItemModel).toBeTruthy('First group\'s property should be instance of CardViewTextItemModel');
expect(cardViewGroup[1].properties[0] instanceof CardViewTextItemModel).toBeTruthy('Second group\'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();
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
property.dataType = 'daemonic:scorcher';
property.defaultValue = 'Daemonic beast';
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
propertyGroups.push(Object.assign({}, propertyGroup));
service.translateToCardViewGroups(propertyGroups, propertyValues);
expect(logService.error).toHaveBeenCalledWith('Unknown type for mapping: daemonic:scorcher');
});
it('should fall back to single-line property type if unrecognised type is found', () => {
property.name = 'FAS:PLAGUE';
property.title = 'The Faro Plague';
property.dataType = 'daemonic:scorcher';
property.defaultValue = 'Daemonic beast';
propertyGroups.push({
title: 'Faro Automated Solutions',
properties: [property]
});
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
});
});
describe('Different types attributes', () => {
beforeEach(() => {
propertyGroups.push(propertyGroup);
});
PropertyGroupTranslatorService.RECOGNISED_ECM_TYPES.forEach((dataType) => {
it(`should translate properly the basic attributes of a property for ${dataType}`, () => {
property.name = 'prefix:name';
property.title = 'title';
property.defaultValue = 'default value';
property.dataType = dataType;
propertyValues = { 'prefix:name': null };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty = cardViewGroup[0].properties[0];
expect(cardViewProperty.label).toBe(property.title);
expect(cardViewProperty.key).toBe('properties.prefix:name');
expect(cardViewProperty.default).toBe(property.defaultValue);
expect(cardViewProperty.editable).toBeTruthy('Property should be editable');
});
});
it('should translate properly the multiline and value attributes for d:text', () => {
property.dataType = 'd:text';
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(cardViewProperty.value).toBe('The Chariot Line');
expect(cardViewProperty.multiline).toBeFalsy('Property should be singleline');
});
it('should translate properly the multiline and value attributes for d:mltext', () => {
property.dataType = 'd:mltext';
propertyValues = { 'FAS:PLAGUE': 'The Chariot Line' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewTextItemModel = <CardViewTextItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewTextItemModel).toBeTruthy('Property should be instance of CardViewTextItemModel');
expect(cardViewProperty.value).toBe('The Chariot Line');
expect(cardViewProperty.multiline).toBeTruthy('Property should be multiline');
});
it('should translate properly the value attribute for d:date', () => {
const expectedValue = new Date().toISOString();
property.dataType = 'd:date';
propertyValues = { 'FAS:PLAGUE': expectedValue };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewDateItemModel = <CardViewDateItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewDateItemModel).toBeTruthy('Property should be instance of CardViewDateItemModel');
expect(cardViewProperty.value).toBe(expectedValue);
});
it('should translate properly the value attribute for d:datetime', () => {
const expectedValue = new Date().toISOString();
property.dataType = 'd:datetime';
propertyValues = { 'FAS:PLAGUE': expectedValue };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewDatetimeItemModel = <CardViewDatetimeItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewDatetimeItemModel).toBeTruthy('Property should be instance of CardViewDatetimeItemModel');
expect(cardViewProperty.value).toBe(expectedValue);
});
it('should translate properly the value attribute for d:int', () => {
property.dataType = 'd:int';
propertyValues = { 'FAS:PLAGUE': '1024' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewIntItemModel = <CardViewIntItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(cardViewProperty.value).toBe(1024);
});
it('should translate properly the value attribute for d:long', () => {
property.dataType = 'd:long';
propertyValues = { 'FAS:PLAGUE': '1024' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewIntItemModel = <CardViewIntItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewIntItemModel).toBeTruthy('Property should be instance of CardViewIntItemModel');
expect(cardViewProperty.value).toBe(1024);
});
it('should translate properly the value attribute for d:float', () => {
property.dataType = 'd:float';
propertyValues = { 'FAS:PLAGUE': '1024.24' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(cardViewProperty.value).toBe(1024.24);
});
it('should translate properly the value attribute for d:double', () => {
property.dataType = 'd:double';
propertyValues = { 'FAS:PLAGUE': '1024.24' };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewFloatItemModel = <CardViewFloatItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewFloatItemModel).toBeTruthy('Property should be instance of CardViewFloatItemModel');
expect(cardViewProperty.value).toBe(1024.24);
});
it('should translate properly the value attribute for d:boolean', () => {
property.dataType = 'd:boolean';
propertyValues = { 'FAS:PLAGUE': true };
const cardViewGroup = service.translateToCardViewGroups(propertyGroups, propertyValues);
const cardViewProperty: CardViewBoolItemModel = <CardViewBoolItemModel> cardViewGroup[0].properties[0];
expect(cardViewProperty instanceof CardViewBoolItemModel).toBeTruthy('Property should be instance of CardViewBoolItemModel');
expect(cardViewProperty.value).toBe(true);
});
});
});

View File

@@ -0,0 +1,143 @@
/*!
* @license
* Copyright 2019 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 {
CardViewItemProperties,
CardViewItem,
CardViewTextItemModel,
CardViewBoolItemModel,
CardViewDateItemModel,
CardViewDatetimeItemModel,
CardViewIntItemModel,
CardViewFloatItemModel,
LogService,
MultiValuePipe,
AppConfigService,
DecimalNumberPipe
} from '@alfresco/adf-core';
import { Property, CardViewGroup, OrganisedPropertyGroup } 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({
providedIn: 'root'
})
export class PropertyGroupTranslatorService {
static readonly RECOGNISED_ECM_TYPES = [D_TEXT, D_MLTEXT, D_DATE, D_DATETIME, D_INT, D_LONG, D_FLOAT, D_DOUBLE, D_BOOLEAN];
valueSeparator: string;
constructor(private logService: LogService,
private multiValuePipe: MultiValuePipe,
private decimalNumberPipe: DecimalNumberPipe,
private appConfig: AppConfigService) {
this.valueSeparator = this.appConfig.get<string>('content-metadata.multi-value-pipe-separator');
}
public translateToCardViewGroups(propertyGroups: OrganisedPropertyGroup[], propertyValues): CardViewGroup[] {
return propertyGroups.map((propertyGroup) => {
const translatedPropertyGroup: any = Object.assign({}, propertyGroup);
translatedPropertyGroup.properties = this.translateArray(propertyGroup.properties, propertyValues);
return translatedPropertyGroup;
});
}
private translateArray(properties: Property[], propertyValues: any): CardViewItem[] {
return properties.map((property) => {
return this.translate(property, propertyValues);
});
}
private translate(property: Property, propertyValues: any): CardViewItem {
let propertyValue;
if (propertyValues && propertyValues[property.name]) {
propertyValue = propertyValues[property.name];
}
this.checkECMTypeValidity(property.dataType);
const prefix = 'properties.';
const propertyDefinition: CardViewItemProperties = {
label: property.title || property.name,
value: propertyValue,
key: `${prefix}${property.name}`,
default: property.defaultValue,
editable: true
};
let cardViewItemProperty;
switch (property.dataType) {
case D_MLTEXT:
cardViewItemProperty = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multiline: true
}));
break;
case D_INT:
case D_LONG:
cardViewItemProperty = new CardViewIntItemModel(propertyDefinition);
break;
case D_FLOAT:
case D_DOUBLE:
cardViewItemProperty = new CardViewFloatItemModel(Object.assign(propertyDefinition, {
pipes: [{ pipe: this.decimalNumberPipe }]
}));
break;
case D_DATE:
cardViewItemProperty = new CardViewDateItemModel(propertyDefinition);
break;
case D_DATETIME:
cardViewItemProperty = new CardViewDatetimeItemModel(propertyDefinition);
break;
case D_BOOLEAN:
cardViewItemProperty = new CardViewBoolItemModel(propertyDefinition);
break;
case D_TEXT:
default:
cardViewItemProperty = new CardViewTextItemModel(Object.assign(propertyDefinition, {
multivalued: property.multiValued,
multiline: property.multiValued,
pipes: [{ pipe: this.multiValuePipe, params: [this.valueSeparator]}]
}));
}
return cardViewItemProperty;
}
private checkECMTypeValidity(ecmPropertyType) {
if (PropertyGroupTranslatorService.RECOGNISED_ECM_TYPES.indexOf(ecmPropertyType) === -1) {
this.logService.error(`Unknown type for mapping: ${ecmPropertyType}`);
}
}
}

View File

@@ -0,0 +1,227 @@
/*!
* @license
* Copyright 2019 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 { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { NodeEntry, Node, SitePaging, Site } from '@alfresco/js-api';
import { AppConfigService, SitesService, setupTestBed } 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 { Subject, of } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
const fakeNodeEntry: NodeEntry = <NodeEntry> {
entry: {
id: 'fake',
name: 'fake-name'
}
};
const fakeNode: Node = <Node> {
id: 'fake',
name: 'fake-name'
};
const fakeSiteList: SitePaging = new 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;
let spyOnDialogOpen: jasmine.Spy;
let afterOpenObservable: Subject<any>;
setupTestBed({
imports: [ContentTestingModule]
});
beforeEach(() => {
const 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);
afterOpenObservable = new Subject<any>();
spyOnDialogOpen = spyOn(materialDialog, 'open').and.returnValue({
afterOpen: () => afterOpenObservable,
afterClosed: () => of({}),
componentInstance: {
error: new Subject<any>()
}
});
});
it('should not open the lock node dialog if have no permission', () => {
const testNode: Node = <Node> {
id: 'fake',
isFile: false
};
service.openLockNodeDialog(testNode).subscribe(() => {
}, (error) => {
expect(error).toBe('OPERATION.FAIL.NODE.NO_PERMISSION');
});
});
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(spyOnDialogOpen).toHaveBeenCalled();
});
it('should NOT be able to open the dialog when node has NOT permission', () => {
service.openCopyMoveDialog('fake-action', fakeNode, 'noperm').subscribe(
() => {
},
(error) => {
expect(spyOnDialogOpen).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(of(fakeNodeEntry));
service.openFileBrowseDialogByFolderId('fake-folder-id').subscribe(() => {
});
tick();
expect(spyOnDialogOpen).toHaveBeenCalled();
}));
it('should be able to open the dialog for files using the first user site', fakeAsync(() => {
spyOn(sitesService, 'getSites').and.returnValue(of(fakeSiteList));
spyOn(documentListService, 'getFolderNode').and.returnValue(of(fakeNodeEntry));
service.openFileBrowseDialogBySite().subscribe(() => {
});
tick();
expect(spyOnDialogOpen).toHaveBeenCalled();
}));
it('should be able to open the dialog for folder using the first user site', fakeAsync(() => {
spyOn(sitesService, 'getSites').and.returnValue(of(fakeSiteList));
spyOn(documentListService, 'getFolderNode').and.returnValue(of(fakeNodeEntry));
service.openFolderBrowseDialogBySite().subscribe(() => {
});
tick();
expect(spyOnDialogOpen).toHaveBeenCalled();
}));
it('should be able to close the material dialog', () => {
spyOn(materialDialog, 'closeAll');
service.close();
expect(materialDialog.closeAll).toHaveBeenCalled();
});
describe('for the copy/move dialog', () => {
const siteNode: Node = <Node> {
id: 'site-node-id',
nodeType: 'st:site'
};
const sites: Node = <Node> {
id: 'sites-id',
nodeType: 'st:sites'
};
const site: Site = <Site> {
id: 'site-id',
guid: 'any-guid'
};
const nodeEntryWithRightPermissions: Node = <Node> {
id: 'node-id',
allowableOperations: ['create']
};
const nodeEntryNoPermissions: Node = <Node> {
id: 'node-id',
allowableOperations: []
};
const siteFixture = [
{
node: siteNode,
expected: false
},
{
node: sites,
expected: false
},
{
node: site,
expected: false
}
];
const permissionFixture = [
{
node: nodeEntryWithRightPermissions,
expected: true
},
{
node: nodeEntryNoPermissions,
expected: false
}
];
let testContentNodeSelectorComponentData;
beforeEach(() => {
spyOnDialogOpen.and.callFake((_: any, config: any) => {
testContentNodeSelectorComponentData = config.data;
return { componentInstance: {} };
});
service.openCopyMoveDialog('fake-action', fakeNode, '!update');
});
it('should NOT allow selection for sites', () => {
expect(spyOnDialogOpen.calls.count()).toEqual(1);
siteFixture.forEach((testData) => {
expect(testContentNodeSelectorComponentData.isSelectionValid(testData.node)).toBe(testData.expected);
});
});
it('should allow selection only for nodes with the right permission', () => {
expect(spyOnDialogOpen.calls.count()).toEqual(1);
permissionFixture.forEach((testData) => {
expect(testContentNodeSelectorComponentData.isSelectionValid(testData.node)).toBe(testData.expected);
});
});
});
});

View File

@@ -0,0 +1,265 @@
/*!
* @license
* Copyright 2019 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 { EventEmitter, Injectable, Output } from '@angular/core';
import { ContentService, ThumbnailService } from '@alfresco/adf-core';
import { Subject, Observable, throwError } from 'rxjs';
import { ShareDataRow } from '../document-list/data/share-data-row.model';
import { Node, NodeEntry, SitePaging } from '@alfresco/js-api';
import { SitesService, TranslationService, AllowableOperationsEnum } 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';
import { NodeLockDialogComponent } from '../dialogs/node-lock.dialog';
import { switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ContentNodeDialogService {
static nonDocumentSiteContent = [
'blog',
'calendar',
'dataLists',
'discussions',
'links',
'wiki'
];
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
constructor(private dialog: MatDialog,
private contentService: ContentService,
private documentListService: DocumentListService,
private siteService: SitesService,
private translation: TranslationService,
private thumbnailService: ThumbnailService) {
}
/**
* Opens a file browser at a chosen folder location.
* @param folderNodeId ID of the folder to use
* @returns Information about the selected file(s)
*/
openFileBrowseDialogByFolderId(folderNodeId: string): Observable<Node[]> {
return this.documentListService.getFolderNode(folderNodeId).pipe(switchMap((nodeEntry: NodeEntry) => {
return this.openUploadFileDialog('Choose', nodeEntry.entry);
}));
}
/**
* Opens a lock node dialog.
* @param contentEntry Node to lock
* @returns Error/status message (if any)
*/
public openLockNodeDialog(contentEntry: Node): Subject<string> {
const observable: Subject<string> = new Subject<string>();
if (this.contentService.hasAllowableOperations(contentEntry, AllowableOperationsEnum.LOCK)) {
this.dialog.open(NodeLockDialogComponent, {
data: {
node: contentEntry,
onError: (error) => {
this.error.emit(error);
observable.error(error);
}
},
width: '400px'
});
} else {
observable.error('OPERATION.FAIL.NODE.NO_PERMISSION');
}
return observable;
}
/**
* Opens a file browser at a chosen site location.
* @returns Information about the selected file(s)
*/
openFileBrowseDialogBySite(): Observable<Node[]> {
return this.siteService.getSites().pipe(switchMap((response: SitePaging) => {
return this.openFileBrowseDialogByFolderId(response.list.entries[0].entry.guid);
}));
}
/**
* Opens a folder browser at a chosen site location.
* @returns Information about the selected folder(s)
*/
openFolderBrowseDialogBySite(): Observable<Node[]> {
return this.openFolderBrowseDialogByFolderId('-my-');
}
/**
* Opens a folder browser at a chosen folder location.
* @param folderNodeId ID of the folder to use
* @returns Information about the selected folder(s)
*/
openFolderBrowseDialogByFolderId(folderNodeId: string): Observable<Node[]> {
return this.documentListService.getFolderNode(folderNodeId).pipe(switchMap((node: NodeEntry) => {
return this.openUploadFolderDialog('Choose', node.entry);
}));
}
/**
* Opens a dialog to copy or move an item to a new location.
* @param action Name of the action (eg, "Copy" or "Move") to show in the title
* @param contentEntry Item to be copied or moved
* @param permission Permission for the operation
* @param excludeSiteContent The site content that should be filtered out
* @returns Information about files that were copied/moved
*/
openCopyMoveDialog(action: string, contentEntry: Node, permission?: string, excludeSiteContent?: string[]): Observable<Node[]> {
if (this.contentService.hasAllowableOperations(contentEntry, permission)) {
const select = new Subject<Node[]>();
select.subscribe({
complete: this.close.bind(this)
});
const title = this.getTitleTranslation(action, contentEntry.name);
const data: ContentNodeSelectorComponentData = {
title: title,
actionName: action,
currentFolderId: contentEntry.parentId,
imageResolver: this.imageResolver.bind(this),
where: '(isFolder=true)',
isSelectionValid: this.isCopyMoveSelectionValid.bind(this),
excludeSiteContent: excludeSiteContent || ContentNodeDialogService.nonDocumentSiteContent,
select: select
};
this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px');
return select;
} else {
const errors = new Error(JSON.stringify({ error: { statusCode: 403 } }));
return throwError(errors);
}
}
/**
* Gets the translation of the dialog title.
* @param action Name of the action to display in the dialog title
* @param name Name of the item on which the action is being performed
* @returns Translated version of the title
*/
getTitleTranslation(action: string, name: string): string {
return this.translation.instant(`NODE_SELECTOR.${action.toUpperCase()}_ITEM`, { name });
}
/**
* Opens a dialog to choose folders to upload.
* @param action Name of the action to show in the title
* @param contentEntry Item to upload
* @returns Information about the chosen folder(s)
*/
openUploadFolderDialog(action: string, contentEntry: Node): Observable<Node[]> {
const select = new Subject<Node[]>();
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.hasAllowableOperationsOnNodeFolder.bind(this),
where: '(isFolder=true)',
select: select
};
this.openContentNodeDialog(data, 'adf-content-node-selector-dialog', '630px');
return select;
}
/**
* Opens a dialog to choose a file to upload.
* @param action Name of the action to show in the title
* @param contentEntry Item to upload
* @returns Information about the chosen file(s)
*/
openUploadFileDialog(action: string, contentEntry: Node): Observable<Node[]> {
const select = new Subject<Node[]>();
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): string | null {
const entry: Node = row.node.entry;
if (!this.contentService.hasAllowableOperations(entry, 'create')) {
if (this.isNodeFolder(entry)) {
return this.thumbnailService.getMimeTypeIcon('disable/folder');
}
}
return null;
}
private isNodeFile(entry: Node): boolean {
return entry.isFile;
}
private hasAllowableOperationsOnNodeFolder(entry: Node): boolean {
return this.isNodeFolder(entry) && this.contentService.hasAllowableOperations(entry, 'create');
}
private isNodeFolder(entry: Node): boolean {
return entry.isFolder;
}
private isCopyMoveSelectionValid(entry: Node): boolean {
return this.hasEntityCreatePermission(entry) && !this.isSite(entry);
}
private hasEntityCreatePermission(entry: Node): boolean {
return this.contentService.hasAllowableOperations(entry, 'create');
}
private isSite(entry) {
return !!entry.guid || entry.nodeType === 'st:site' || entry.nodeType === 'st:sites';
}
/** Closes the currently open dialog. */
close() {
this.dialog.closeAll();
}
}

View File

@@ -0,0 +1,101 @@
<div class="adf-content-node-selector-content" (node-select)="onNodeSelect($event)">
<mat-form-field floatPlaceholder="never" class="adf-content-node-selector-content-input">
<input matInput
id="searchInput"
[formControl]="searchInput"
type="text"
placeholder="{{'NODE_SELECTOR.SEARCH' | translate}}"
[value]="searchTerm"
data-automation-id="content-node-selector-search-input">
<mat-icon *ngIf="searchTerm.length > 0"
matSuffix (click)="clear()"
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-clear">clear
</mat-icon>
<mat-icon *ngIf="searchTerm.length === 0"
matSuffix
class="adf-content-node-selector-content-input-icon"
data-automation-id="content-node-selector-search-icon">search
</mat-icon>
</mat-form-field>
<adf-sites-dropdown
class="full-width"
(change)="siteChanged($event)"
[placeholder]="'NODE_SELECTOR.SELECT_LOCATION'"
[hideMyFiles]="dropdownHideMyFiles"
[siteList]="dropdownSiteList"
data-automation-id="content-node-selector-sites-combo">
</adf-sites-dropdown>
<adf-toolbar>
<adf-toolbar-title>
<ng-container *ngIf="!showBreadcrumbs()">
<span role="heading" aria-level="3" class="adf-search-results-label">{{ 'NODE_SELECTOR.SEARCH_RESULTS' | translate }}</span>
</ng-container>
<adf-dropdown-breadcrumb *ngIf="showBreadcrumbs()"
class="adf-content-node-selector-content-breadcrumb"
(navigate)="clearSearch()"
[target]="documentList"
[transform]="breadcrumbTransform"
[folderNode]="breadcrumbFolderNode"
[root]="breadcrumbFolderTitle"
data-automation-id="content-node-selector-content-breadcrumb">
</adf-dropdown-breadcrumb>
</adf-toolbar-title>
</adf-toolbar>
<div
class="adf-content-node-selector-content-list"
[class.adf-content-node-selector-content-list-searchLayout]="showingSearchResults"
data-automation-id="content-node-selector-content-list">
<adf-document-list
#documentList
[adf-highlight]="searchTerm"
adf-highlight-selector=".adf-name-location-cell-name"
[showHeader]="false"
[node]="nodePaging"
[maxItems]="pageSize"
[rowFilter]="_rowFilter"
[imageResolver]="imageResolver"
[currentFolderId]="folderIdToShow"
selectionMode="single"
[contextMenuActions]="false"
[contentActions]="false"
[allowDropFiles]="false"
[sorting]="'server'"
[where]="where"
(folderChange)="onFolderChange()"
(ready)="onFolderLoaded()"
data-automation-id="content-node-selector-document-list">
<adf-custom-empty-content-template>
<div>{{ 'NODE_SELECTOR.NO_RESULTS' | translate }}</div>
</adf-custom-empty-content-template>
<data-columns>
<data-column key="$thumbnail" type="image"></data-column>
<data-column key="name" type="text" class="adf-full-width adf-ellipsis-cell">
<ng-template let-context>
<adf-name-location-cell [row]="context.row"></adf-name-location-cell>
</ng-template>
</data-column>
<data-column key="modifiedAt" type="date" format="timeAgo" class="adf-content-selector-modified-cell"></data-column>
<data-column key="createdByUser.displayName" type="text" class="adf-content-selector-modifier-cell"></data-column>
<data-column key="visibility" type="text" class="adf-content-selector-visibility-cell"></data-column>
</data-columns>
</adf-document-list>
<adf-infinite-pagination
[target]="target"
[loading]="loadingSearchResults"
(loadMore)="getNextPageOfSearch($event)"
data-automation-id="content-node-selector-search-pagination">
{{ 'ADF-DOCUMENT-LIST.LAYOUT.LOAD_MORE' | translate }}
</adf-infinite-pagination>
</div>
</div>

View File

@@ -0,0 +1,190 @@
@mixin adf-content-node-selector-theme($theme) {
$primary: map-get($theme, primary);
$foreground: map-get($theme, foreground);
$background: map-get($theme, background);
$content-node-selector-thumbnail-width: 35px !default;
.adf-search-results-label {
font-weight: 600;
font-size: 14px;
font-style: normal;
font-stretch: normal;
line-height: 1.43;
letter-spacing: -0.2px;
color: mat-color($foreground, base, 0.87);
}
.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-form-field-underline .mat-form-field-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 {
max-height: 48px;
border-bottom-width: 0;
font-size: 14px;
&.mat-toolbar-single-row {
height: auto;
}
}
&-breadcrumb {
.adf-dropdown-breadcrumb-trigger {
outline: none;
.mat-icon {
color: mat-color($foreground, base, 0.45);
&:hover {
color: mat-color($foreground, base, 0.65);
}
}
}
.adf-dropdown-breadcrumb-item-chevron {
color: mat-color($foreground, base, 0.45);
}
}
&-list {
height: 200px;
overflow: auto;
border: 1px solid mat-color($foreground, base, 0.07);
border-top: 0;
.adf-highlight {
color: mat-color($primary);
}
.adf-datatable-list {
border: none;
.adf-datatable-selected {
height: 100%;
width: 100%;
}
.adf-datatable-selected > svg {
fill: #00bcd4 !important;
}
.adf-no-content-container {
text-align: center;
border: none !important;
}
.adf-datatable-cell {
&--image {
min-width: $content-node-selector-thumbnail-width;
width: $content-node-selector-thumbnail-width;
max-width: $content-node-selector-thumbnail-width;
}
&:nth-child(2) {
flex: 1 0 95px;
}
.adf-name-location-cell-location {
display: none;
}
&.adf-content-selector-visibility-cell {
flex: 0 1 auto;
min-width: 1px;
.adf-datatable-cell-value {
padding: 0;
}
}
}
.adf-datatable-body .adf-datatable-row {
min-height: 40px;
@media screen and (-ms-high-contrast: active),
screen and (-ms-high-contrast: none) {
padding-top: 15px;
}
&:first-child {
.adf-datatable-cell {
border-top: none;
}
}
&:last-child {
.adf-datatable-cell {
border-bottom: none;
}
}
}
}
&-searchLayout {
.adf-datatable-list .adf-datatable-row {
min-height: 65px !important;
.adf-datatable-cell {
.adf-name-location-cell-location {
padding: 0 10px;
display: block;
}
.adf-name-location-cell-name {
padding: 5px 10px 2px;
}
&.adf-content-selector-modified-cell {
display: none;
}
&.adf-content-selector-modifier-cell {
display: none;
}
&.adf-content-selector-visibility-cell {
display: none;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,430 @@
/*!
* @license
* Copyright 2019 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, OnDestroy } from '@angular/core';
import {
HighlightDirective,
UserPreferencesService,
PaginationModel,
UserPreferenceValues,
InfinitePaginationComponent, PaginatedComponent
} from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { Node, NodePaging, Pagination, SiteEntry, SitePaging } from '@alfresco/js-api';
import { DocumentListComponent } 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, takeUntil } from 'rxjs/operators';
import { CustomResourcesService } from '../document-list/services/custom-resources.service';
import { ShareDataRow } from '../document-list';
import { Subject } from 'rxjs';
export type ValidationFunction = (entry: Node) => 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,
host: { 'class': 'adf-content-node-selector-panel' }
})
export class ContentNodeSelectorPanelComponent implements OnInit, OnDestroy {
DEFAULT_PAGINATION: Pagination = new Pagination({
maxItems: 25,
skipCount: 0,
totalItems: 0,
hasMoreItems: false
});
/** Node ID of the folder currently listed. */
@Input()
currentFolderId: string = null;
/** Hide the "My Files" option added to the site list by default.
* See the [Sites Dropdown component](sites-dropdown.component.md)
* for more information.
*/
@Input()
dropdownHideMyFiles: boolean = false;
/** Custom site for site dropdown. This is the same as the `siteList`.
* property of the Sites Dropdown component (see its doc page
* for more information).
*/
@Input()
dropdownSiteList: SitePaging = null;
_rowFilter: RowFilter = defaultValidation;
/** Custom *where* filter function. See the
* Document List component
* for more information.
*/
@Input()
where: string;
/**
* Custom row filter function. See the
* [Row Filter Model](row-filter.model.md) page
* for more information.
*/
@Input()
set rowFilter(rowFilter: RowFilter) {
this.createRowFilter(rowFilter);
}
get rowFilter(): RowFilter {
return this._rowFilter;
}
_excludeSiteContent: string[] = [];
/** Custom list of site content componentIds.
* Used to filter out the corresponding items from the displayed nodes
*/
@Input()
set excludeSiteContent(excludeSiteContent: string[]) {
this._excludeSiteContent = excludeSiteContent;
this.createRowFilter(this._rowFilter);
}
get excludeSiteContent(): string[] {
return this._excludeSiteContent;
}
/**
* Custom image resolver function. See the
* [Image Resolver Model](image-resolver.model.md) page
* for more information.
*/
@Input()
imageResolver: ImageResolver = null;
/** Number of items shown per page in the list. */
@Input()
pageSize: number = this.DEFAULT_PAGINATION.maxItems;
/** Function used to decide if the selected node has permission to be selected.
* Default value is a function that always returns true.
*/
@Input()
isSelectionValid: ValidationFunction = defaultValidation;
/** Transformation to be performed on the chosen/folder node before building the
* breadcrumb UI. Can be useful when custom formatting is needed for the breadcrumb.
* You can change the path elements from the node that are used to build the
* breadcrumb using this function.
*/
@Input()
breadcrumbTransform: (node) => any;
/** Emitted when the user has chosen an item. */
@Output()
select: EventEmitter<Node[]> = new EventEmitter<Node[]>();
@ViewChild('documentList')
documentList: DocumentListComponent;
@ViewChild(HighlightDirective)
highlighter: HighlightDirective;
nodePaging: NodePaging | null = null;
siteId: null | string;
searchTerm: string = '';
showingSearchResults: boolean = false;
loadingSearchResults: boolean = false;
inDialog: boolean = false;
_chosenNode: Node = null;
folderIdToShow: string | null = null;
breadcrumbFolderTitle: string | null = null;
pagination: PaginationModel = this.DEFAULT_PAGINATION;
@ViewChild(InfinitePaginationComponent)
infinitePaginationComponent: InfinitePaginationComponent;
infiniteScroll: boolean = false;
debounceSearch: number = 200;
searchInput: FormControl = new FormControl();
target: PaginatedComponent;
private onDestroy$ = new Subject<boolean>();
constructor(private contentNodeSelectorService: ContentNodeSelectorService,
private customResourcesService: CustomResourcesService,
private userPreferencesService: UserPreferencesService) {
}
set chosenNode(value: Node) {
this._chosenNode = value;
let valuesArray = null;
if (value) {
valuesArray = [value];
}
this.select.next(valuesArray);
}
get chosenNode() {
return this._chosenNode;
}
ngOnInit() {
this.searchInput.valueChanges
.pipe(
debounceTime(this.debounceSearch),
takeUntil(this.onDestroy$)
)
.subscribe(searchValue => this.search(searchValue));
this.userPreferencesService
.select(UserPreferenceValues.PaginationSize)
.pipe(takeUntil(this.onDestroy$))
.subscribe(pagSize => this.pageSize = pagSize);
this.target = this.documentList;
this.folderIdToShow = this.currentFolderId;
this.breadcrumbTransform = this.breadcrumbTransform ? this.breadcrumbTransform : null;
this.isSelectionValid = this.isSelectionValid ? this.isSelectionValid : defaultValidation;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private createRowFilter(filter?: RowFilter) {
if (!filter) {
filter = () => true;
}
this._rowFilter = (value: ShareDataRow, index: number, array: ShareDataRow[]) => {
return filter(value, index, array) &&
!this.isExcludedSiteContent(value);
};
}
private isExcludedSiteContent(row: ShareDataRow): boolean {
const entry = row.node.entry;
if (this._excludeSiteContent && this._excludeSiteContent.length &&
entry &&
entry.properties &&
entry.properties['st:componentId']) {
const excludedItem = this._excludeSiteContent.find(
(id: string) => entry.properties['st:componentId'] === id
);
return !!excludedItem;
}
return false;
}
/**
* 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.setTitleIfCustomSite(chosenSite);
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 the actually selected|entered folder node or null in case of searching for the breadcrumb
*/
get breadcrumbFolderNode(): Node | null {
let folderNode: Node;
if (this.showingSearchResults && this.chosenNode) {
folderNode = this.chosenNode;
} else {
folderNode = this.documentList.folderNode;
}
return folderNode;
}
/**
* Clear the search input and reset to last folder node in which search was performed
*/
clear(): void {
this.clearSearch();
this.folderIdToShow = this.siteId || this.currentFolderId;
}
/**
* Clear the search input and search related data
*/
clearSearch() {
this.searchTerm = '';
this.nodePaging = null;
this.pagination.maxItems = this.pageSize;
this.chosenNode = null;
this.showingSearchResults = false;
}
/**
* Update the result list depending on the criteria
*/
private updateResults(): void {
this.target = this.searchTerm.length > 0 ? null : this.documentList;
if (this.searchTerm.length === 0) {
this.clear();
} else {
this.startNewSearch();
}
}
/**
* Load the first page of a new search result
*/
private startNewSearch(): void {
this.nodePaging = null;
this.pagination.maxItems = this.pageSize;
if (this.target) {
this.infinitePaginationComponent.reset();
}
this.chosenNode = null;
this.folderIdToShow = null;
this.querySearch();
}
/**
* Perform the call to searchService with the proper parameters
*/
private querySearch(): void {
this.loadingSearchResults = true;
if (this.customResourcesService.hasCorrespondingNodeIds(this.siteId)) {
this.customResourcesService.getCorrespondingNodeIds(this.siteId)
.subscribe((nodeIds) => {
this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.pagination.skipCount, this.pagination.maxItems, nodeIds)
.subscribe(this.showSearchResults.bind(this));
},
() => {
this.showSearchResults({ list: { entries: [] } });
});
} else {
this.contentNodeSelectorService.search(this.searchTerm, this.siteId, this.pagination.skipCount, this.pagination.maxItems)
.subscribe(this.showSearchResults.bind(this));
}
}
/**
* Show the results of the search
*
* @param results Search results
*/
private showSearchResults(nodePaging: NodePaging): void {
this.showingSearchResults = true;
this.loadingSearchResults = false;
this.nodePaging = nodePaging;
}
/**
* Sets showingSearchResults state to be able to differentiate between search results or folder results
*/
onFolderChange(): void {
this.showingSearchResults = false;
this.infiniteScroll = false;
this.clearSearch();
}
/**
* Attempts to set the currently loaded node
*/
onFolderLoaded(): void {
if (!this.showingSearchResults) {
this.attemptNodeSelection(this.documentList.folderNode);
}
}
/**
* Returns whether breadcrumb has to be shown or not
*/
showBreadcrumbs() {
return !this.showingSearchResults || this.chosenNode;
}
/**
* Loads the next batch of search results
*
* @param event Pagination object
*/
getNextPageOfSearch(pagination: Pagination): void {
this.infiniteScroll = true;
this.pagination = pagination;
if (this.searchTerm.length > 0) {
this.querySearch();
}
}
/**
* Selects node as chosen if it has the right permission, clears the selection otherwise
*
* @param entry
*/
private attemptNodeSelection(entry: Node): void {
if (entry && 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);
}
setTitleIfCustomSite(site: SiteEntry) {
if (this.customResourcesService.isCustomSource(site.entry.guid)) {
this.breadcrumbFolderTitle = site.entry.title;
} else {
this.breadcrumbFolderTitle = null;
}
}
}

View File

@@ -0,0 +1,34 @@
/*!
* @license
* Copyright 2019 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 { Node, SitePaging } from '@alfresco/js-api';
import { Subject } from 'rxjs';
export interface ContentNodeSelectorComponentData {
title: string;
actionName?: string;
currentFolderId: string;
dropdownHideMyFiles?: boolean;
dropdownSiteList?: SitePaging;
rowFilter?: any;
where?: string;
imageResolver?: any;
isSelectionValid?: (entry: Node) => boolean;
breadcrumbTransform?: (node) => any;
excludeSiteContent?: string[];
select: Subject<Node[]>;
}

View File

@@ -0,0 +1,35 @@
<header
mat-dialog-title
data-automation-id="content-node-selector-title">
<h2>{{data?.title}}</h2>
</header>
<mat-dialog-content>
<adf-content-node-selector-panel
[currentFolderId]="data?.currentFolderId"
[dropdownHideMyFiles]="data?.dropdownHideMyFiles"
[dropdownSiteList]="data?.dropdownSiteList"
[rowFilter]="data?.rowFilter"
[imageResolver]="data?.imageResolver"
[isSelectionValid]="data?.isSelectionValid"
[breadcrumbTransform]="data?.breadcrumbTransform"
[excludeSiteContent]="data?.excludeSiteContent"
[where]="data?.where"
(select)="onSelect($event)">
</adf-content-node-selector-panel>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button
mat-button
(click)="close()"
data-automation-id="content-node-selector-actions-cancel">{{ 'NODE_SELECTOR.CANCEL' | translate }}
</button>
<button mat-button
[disabled]="!chosenNode"
class="adf-choose-action"
(click)="onClick()"
data-automation-id="content-node-selector-actions-choose">{{ buttonActionName | translate }}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,59 @@
@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-title {
margin-left: 24px;
margin-right: 24px;
font-size: 20px;
font-weight: 600;
font-style: normal;
font-stretch: normal;
line-height: 1.6;
letter-spacing: -0.5px;
color: mat-color($foreground, text, 0.87);
h2 {
margin: unset;
font-size: unset;
}
}
.mat-dialog-container {
padding-left: 0;
padding-right: 0;
}
.mat-dialog-content {
margin: 0;
overflow: hidden;
}
.mat-dialog-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;
font-weight: normal;
}
.adf-choose-action {
&[disabled] {
opacity: 0.6;
}
&:enabled {
color: mat-color($primary);
}
}
}
}
}

View File

@@ -0,0 +1,163 @@
/*!
* @license
* Copyright 2019 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 { MAT_DIALOG_DATA } from '@angular/material';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { EventEmitter } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { Node } from '@alfresco/js-api';
import { By } from '@angular/platform-browser';
import { setupTestBed, SitesService } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { DocumentListService } from '../document-list/services/document-list.service';
import { DocumentListComponent } from '../document-list/components/document-list.component';
import { ShareDataRow } from '../document-list';
describe('ContentNodeSelectorDialogComponent', () => {
let component: ContentNodeSelectorComponent;
let fixture: ComponentFixture<ContentNodeSelectorComponent>;
const data: any = {
title: 'Move along citizen...',
actionName: 'move',
select: new EventEmitter<Node>(),
rowFilter: (shareDataRow: ShareDataRow) => shareDataRow.node.entry.name === 'impossible-name',
imageResolver: () => 'piccolo',
currentFolderId: 'cat-girl-nuku-nuku'
};
setupTestBed({
imports: [ContentTestingModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
beforeEach(() => {
const documentListService: DocumentListService = TestBed.get(DocumentListService);
const sitesService: SitesService = TestBed.get(SitesService);
spyOn(documentListService, 'getFolder').and.returnValue(of({ list: [] }));
spyOn(documentListService, 'getFolderNode').and.returnValue(of({ entry: {} }));
spyOn(sitesService, 'getSites').and.returnValue(of({ list: { entries: [] } }));
fixture = TestBed.createComponent(ContentNodeSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
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(component.buttonActionName).toBe('NODE_SELECTOR.MOVE');
expect(actionButton).not.toBeNull();
expect(actionButton.nativeElement.innerText).toBe('NODE_SELECTOR.MOVE');
});
it('should pass through the injected currentFolderId to the documentList', () => {
const 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', () => {
const documentList = fixture.debugElement.query(By.directive(DocumentListComponent));
expect(documentList).not.toBeNull('Document list should be shown');
expect(documentList.componentInstance.rowFilter({
node: {
entry: new Node({
name: 'impossible-name',
id: 'name'
})
}
}))
.toBe(data.rowFilter(<ShareDataRow> {
node: {
entry: new Node({
name: 'impossible-name',
id: 'name'
})
}
}));
});
it('should pass through the injected imageResolver to the documentList', () => {
const 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();
const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBeTruthy();
});
it('should be enabled when a node is chosen', () => {
component.onSelect([new Node({ id: 'fake' })]);
fixture.detectChanges();
const actionButton = fixture.debugElement.query(By.css('[data-automation-id="content-node-selector-actions-choose"]'));
expect(actionButton.nativeElement.disabled).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,50 @@
/*!
* @license
* Copyright 2019 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, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material';
import { Node } from '@alfresco/js-api';
import { ContentNodeSelectorComponentData } from './content-node-selector.component-data.interface';
@Component({
selector: 'adf-content-node-selector',
templateUrl: './content-node-selector.component.html',
styleUrls: ['./content-node-selector.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ContentNodeSelectorComponent {
buttonActionName: string;
chosenNode: Node[];
constructor(@Inject(MAT_DIALOG_DATA) public data: ContentNodeSelectorComponentData) {
this.buttonActionName = data.actionName ? `NODE_SELECTOR.${data.actionName.toUpperCase()}` : 'NODE_SELECTOR.CHOOSE';
}
close() {
this.data.select.complete();
}
onSelect(nodeList: Node[]) {
this.chosenNode = nodeList;
}
onClick(): void {
this.data.select.next(this.chosenNode);
this.data.select.complete();
}
}

View File

@@ -0,0 +1,57 @@
/*!
* @license
* Copyright 2019 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 { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module';
import { ContentNodeSelectorPanelComponent } from './content-node-selector-panel.component';
import { ContentNodeSelectorComponent } from './content-node-selector.component';
import { SitesDropdownModule } from '../site-dropdown/sites-dropdown.module';
import { BreadcrumbModule } from '../breadcrumb/breadcrumb.module';
import { CoreModule } from '@alfresco/adf-core';
import { DocumentListModule } from '../document-list/document-list.module';
import { NameLocationCellComponent } from './name-location-cell/name-location-cell.component';
@NgModule({
imports: [
FormsModule,
ReactiveFormsModule,
CoreModule,
CommonModule,
MaterialModule,
SitesDropdownModule,
BreadcrumbModule,
DocumentListModule
],
exports: [
ContentNodeSelectorPanelComponent,
NameLocationCellComponent,
ContentNodeSelectorComponent
],
entryComponents: [
ContentNodeSelectorPanelComponent,
ContentNodeSelectorComponent
],
declarations: [
ContentNodeSelectorPanelComponent,
NameLocationCellComponent,
ContentNodeSelectorComponent
]
})
export class ContentNodeSelectorModule {}

View File

@@ -0,0 +1,124 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { QueryBody } from '@alfresco/js-api';
import { SearchService, setupTestBed } from '@alfresco/adf-core';
import { ContentNodeSelectorService } from './content-node-selector.service';
import { ContentTestingModule } from '../testing/content.testing.module';
class SearchServiceMock {
public query: QueryBody;
searchByQueryBody(query: QueryBody) {
this.query = query;
}
}
describe('ContentNodeSelectorService', () => {
let service: ContentNodeSelectorService;
let search: SearchServiceMock;
setupTestBed({
imports: [ContentTestingModule],
providers: [
{ provide: SearchService, useClass: SearchServiceMock }
]
});
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', 'properties']);
});
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\'' });
});
it('should filter for the extra provided ancestors if defined', () => {
service.search('nuka cola quantum', 'diamond-city', 0, 25, ['extra-diamond-city']);
expect(search.query.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\' OR ANCESTOR:\'workspace://SpacesStore/extra-diamond-city\'' });
});
it('should NOT filter for extra ancestors if an empty list of ids is provided', () => {
service.search('nuka cola quantum', 'diamond-city', 0, 25, []);
expect(search.query.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' });
});
it('should NOT filter for the extra provided ancestor if it\'s the same as the rootNodeId', () => {
service.search('nuka cola quantum', 'diamond-city', 0, 25, ['diamond-city']);
expect(search.query.filterQueries).toContain({ query: 'ANCESTOR:\'workspace://SpacesStore/diamond-city\'' });
});
});

View File

@@ -0,0 +1,80 @@
/*!
* @license
* Copyright 2019 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 { SearchService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { ResultSetPaging } from '@alfresco/js-api';
import { Observable } from 'rxjs';
/**
* Internal service used by ContentNodeSelector component.
*/
@Injectable({
providedIn: 'root'
})
export class ContentNodeSelectorService {
constructor(private searchService: SearchService) {
}
/**
* Performs a search for content node selection
*
* @param searchTerm The term to search for
* @param rootNodeId The root is to start the search from
* @param skipCount From where to start the loading
* @param maxItems How many items to load
* @param [extraNodeIds] List of extra node ids to search from. This last parameter is necessary when
* the rootNodeId is one of the supported aliases (e.g. '-my-', '-root-', '-mysites-', etc.)
* and search is not supported for that alias, but can be performed on its corresponding nodes.
*/
public search(searchTerm: string, rootNodeId: string = null, skipCount: number = 0, maxItems: number = 25, extraNodeIds?: string[]): Observable<ResultSetPaging> {
let extraParentFiltering = '';
if (extraNodeIds && extraNodeIds.length) {
extraNodeIds
.filter((id) => id !== rootNodeId)
.forEach((extraId) => {
extraParentFiltering += ` OR ANCESTOR:'workspace://SpacesStore/${extraId}'`;
});
}
const parentFiltering = rootNodeId ? [{ query: `ANCESTOR:'workspace://SpacesStore/${rootNodeId}'${extraParentFiltering}` }] : [];
const defaultSearchNode: any = {
query: {
query: `${searchTerm}* OR name:${searchTerm}*`
},
include: ['path', 'allowableOperations', 'properties'],
paging: {
maxItems: maxItems,
skipCount: skipCount
},
filterQueries: [
{ query: "TYPE:'cm:folder'" },
{ query: 'NOT cm:creator:System' },
...parentFiltering
],
scope: {
locations: ['nodes']
}
};
return this.searchService.searchByQueryBody(defaultSearchNode);
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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';

View File

@@ -0,0 +1,11 @@
@mixin adf-name-location-cell-theme($theme) {
$foreground: map-get($theme, foreground);
.adf-name-location-cell {
display: grid;
&-location {
color: mat-color($foreground, base, 0.5);
font-size: 12px;
}
}
}

View File

@@ -0,0 +1,64 @@
/*!
* @license
* Copyright 2019 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { NameLocationCellComponent } from './name-location-cell.component';
import { By } from '@angular/platform-browser';
import { DataRow, setupTestBed } from '@alfresco/adf-core';
describe('NameLocationCellComponent', () => {
let component: NameLocationCellComponent;
let fixture: ComponentFixture<NameLocationCellComponent>;
let rowData: DataRow;
setupTestBed({
declarations: [
NameLocationCellComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(NameLocationCellComponent);
component = fixture.componentInstance;
rowData = <DataRow> {
getValue(key): any {
if (key === 'name') {
return 'file-name';
} else if (key === 'path') {
return { name: '/path/to/location' };
}
return undefined;
}
};
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');
});
});

View File

@@ -0,0 +1,50 @@
/*!
* @license
* Copyright 2019 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 { Input, ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { DataRow } from '@alfresco/adf-core';
@Component({
selector: 'adf-name-location-cell',
template: `
<div class="adf-name-location-cell-name adf-datatable-cell-value" [title]="name">{{ name }}</div>
<div class="adf-name-location-cell-location adf-datatable-cell-value" [title]="path">{{ path }}</div>
`,
styleUrls: ['./name-location-cell.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'adf-name-location-cell adf-datatable-content-cell' }
})
export class NameLocationCellComponent implements OnInit {
name: string = '';
path: string = '';
@Input()
row: DataRow;
ngOnInit() {
if (this.row) {
this.name = this.row.getValue('name');
const fullPath = this.row.getValue('path');
if (fullPath) {
this.path = fullPath.name || '';
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 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 './name-location-cell/name-location-cell.component';
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';
export * from './content-node-selector.module';

View File

@@ -0,0 +1,55 @@
<div class="adf-share-link__dialog-content">
<h1 data-automation-id="adf-share-dialog-title" class="adf-share-link__title">
{{ 'SHARE.DIALOG-TITLE' | translate }} {{ fileName }}
</h1>
<mat-dialog-content>
<p class="adf-share-link__info">{{ 'SHARE.DESCRIPTION' | translate }}</p>
<div class="adf-share-link--row">
<h1 class="adf-share-link__label">{{ 'SHARE.TITLE' | translate }}</h1>
<mat-slide-toggle color="primary" data-automation-id="adf-share-toggle" [checked]="isFileShared"
[disabled]="!canUpdate || isDisabled" (change)="onSlideShareChange($event)">
</mat-slide-toggle>
</div>
<form [formGroup]="form">
<mat-form-field class="adf-full-width">
<input #sharedLinkInput data-automation-id="adf-share-link" class="adf-share-link__input" matInput
cdkFocusInitial placeholder="{{ 'SHARE.PUBLIC-LINK' | translate }}" formControlName="sharedUrl"
readonly="readonly">
<mat-icon class="adf-input-action" matSuffix
[clipboard-notification]="'SHARE.CLIPBOARD-MESSAGE' | translate" [adf-clipboard]
[target]="sharedLinkInput">
link
</mat-icon>
</mat-form-field>
<div class="adf-share-link--row">
<h1 class="adf-share-link__label">{{ 'SHARE.EXPIRES' | translate }}</h1>
<mat-slide-toggle [disabled]="!canUpdate" #slideToggleExpirationDate color="primary"
data-automation-id="adf-expire-toggle" [checked]="form.controls['time'].value"
(change)="onToggleExpirationDate($event)">
</mat-slide-toggle>
</div>
<mat-form-field class="adf-full-width">
<mat-datetimepicker-toggle #matDatetimepickerToggle="matDatetimepickerToggle" [for]="datetimePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker (closed)="onDatetimepickerClosed()" [type]="type" openOnFocus="true" timeInterval="1"></mat-datetimepicker>
<input class="adf-share-link__input"
#dateTimePickerInput
matInput
[min]="minDate"
formControlName="time"
[matDatetimepicker]="datetimePicker" />
</mat-form-field>
</form>
</mat-dialog-content>
<div mat-dialog-actions>
<button data-automation-id="adf-share-dialog-close" mat-button color="primary" mat-dialog-close>
{{ 'SHARE.CLOSE' | translate }}
</button>
</div>
</div>

View File

@@ -0,0 +1,69 @@
@mixin adf-share-link-typography {
letter-spacing: -0.4px;
line-height: 2;
font-weight: normal;
font-style: normal;
font-stretch: normal;
font-size: 16px;
opacity: 0.87;
}
.adf-share-link-dialog {
.adf-share-link {
&__dialog-content {
display: flex;
flex-direction: column;
}
&__label {
@include adf-share-link-typography;
flex: 1 1 auto;
}
&__title {
@include adf-share-link-typography;
}
&__info {
@include adf-share-link-typography;
opacity: 0.54;
font-size: 13px;
}
&--row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
&__input {
opacity: 0.54;
}
}
.adf-input-action {
cursor: pointer;
}
.adf-full-width {
width: 100%;
}
.mat-form-field-infix {
border-top: unset;
}
.mat-dialog-actions {
justify-content: flex-end;
& > button {
text-transform: uppercase;
}
}
.mat-form-field-flex {
align-items: center;
}
}

View File

@@ -0,0 +1,356 @@
/*!
* @license
* Copyright 2019 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed, fakeAsync, async, ComponentFixture } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material';
import { of, empty } from 'rxjs';
import {
setupTestBed,
SharedLinksApiService,
NodesApiService,
NotificationService,
RenditionsService,
AppConfigService
} from '@alfresco/adf-core';
import { CoreModule, AppConfigServiceMock } from '@alfresco/adf-core';
import { ContentNodeShareModule } from './content-node-share.module';
import { ShareDialogComponent } from './content-node-share.dialog';
import moment from 'moment-es6';
describe('ShareDialogComponent', () => {
let node;
let matDialog: MatDialog;
const notificationServiceMock = {
openSnackMessage: jasmine.createSpy('openSnackMessage')
};
let sharedLinksApiService: SharedLinksApiService;
let renditionService: RenditionsService;
let nodesApiService: NodesApiService;
let fixture: ComponentFixture<ShareDialogComponent>;
let component: ShareDialogComponent;
let appConfigService: AppConfigService;
setupTestBed({
imports: [
NoopAnimationsModule,
CoreModule.forRoot(),
ContentNodeShareModule
],
providers: [
NodesApiService,
SharedLinksApiService,
{ provide: AppConfigService, useClass: AppConfigServiceMock },
{ provide: NotificationService, useValue: notificationServiceMock },
{ provide: MatDialogRef, useValue: { close: () => {}} },
{ provide: MAT_DIALOG_DATA, useValue: {} }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(ShareDialogComponent);
matDialog = TestBed.get(MatDialog);
sharedLinksApiService = TestBed.get(SharedLinksApiService);
renditionService = TestBed.get(RenditionsService);
nodesApiService = TestBed.get(NodesApiService);
appConfigService = TestBed.get(AppConfigService);
component = fixture.componentInstance;
node = {
entry: {
id: 'nodeId',
allowableOperations: ['update'],
isFile: true,
properties: {}
}
};
});
describe('Error Handling', () => {
it('should emit a generic error when unshare fails', (done) => {
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(
of(new Error(`{ "error": { "statusCode": 999 } }`))
);
const sub = sharedLinksApiService.error.subscribe((err) => {
expect(err.statusCode).toBe(999);
expect(err.message).toBe('SHARE.UNSHARE_ERROR');
sub.unsubscribe();
done();
});
component.deleteSharedLink('guid');
});
it('should emit permission error when unshare fails', (done) => {
spyOn(sharedLinksApiService, 'deleteSharedLink').and.returnValue(
of(new Error(`{ "error": { "statusCode": 403 } }`))
);
const sub = sharedLinksApiService.error.subscribe((err) => {
expect(err.statusCode).toBe(403);
expect(err.message).toBe('SHARE.UNSHARE_PERMISSION_ERROR');
sub.unsubscribe();
done();
});
component.deleteSharedLink('guid');
});
});
it(`should toggle share action when property 'sharedId' does not exists`, () => {
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({
entry: { id: 'sharedId', sharedId: 'sharedId' }
}));
spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty());
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
expect(sharedLinksApiService.createSharedLinks).toHaveBeenCalled();
expect(renditionService.generateRenditionForNode).toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked');
});
it(`should not toggle share action when file has 'sharedId' property`, async(() => {
spyOn(sharedLinksApiService, 'createSharedLinks');
spyOn(renditionService, 'generateRenditionForNode').and.returnValue(empty());
node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(sharedLinksApiService.createSharedLinks).not.toHaveBeenCalled();
expect(fixture.nativeElement.querySelector('input[formcontrolname="sharedUrl"]').value).toBe('some-url/sharedId');
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-checked');
});
}));
it('should open a confirmation dialog when unshare button is triggered', () => {
spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) });
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label')
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(matDialog.open).toHaveBeenCalled();
});
it('should unshare file when confirmation dialog returns true', fakeAsync(() => {
spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(true) });
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label')
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(sharedLinksApiService.deleteSharedLink).toHaveBeenCalled();
}));
it('should not unshare file when confirmation dialog returns false', fakeAsync(() => {
spyOn(matDialog, 'open').and.returnValue({ beforeClose: () => of(false) });
spyOn(sharedLinksApiService, 'deleteSharedLink').and.callThrough();
node.entry.properties['qshare:sharedId'] = 'sharedId';
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.nativeElement.querySelector('.mat-slide-toggle label')
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(sharedLinksApiService.deleteSharedLink).not.toHaveBeenCalled();
}));
it('should not allow unshare when node has no update permission', () => {
node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.allowableOperations = [];
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.mat-slide-toggle').classList).toContain('mat-disabled');
expect(fixture.nativeElement.querySelector('input[formcontrolname="time"]').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('mat-datetimepicker-toggle button').disabled).toBe(true);
});
it('should reset expiration date when toggle is unchecked', () => {
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.properties['qshare:sharedId'] = '2017-04-15T18:31:37+00:00';
node.entry.allowableOperations = ['update'];
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
component.form.controls['time'].setValue(moment());
fixture.detectChanges();
fixture.nativeElement
.querySelector(
'.mat-slide-toggle[data-automation-id="adf-expire-toggle"] label'
)
.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': null }
});
expect(
fixture.nativeElement.querySelector('input[formcontrolname="time"]').value
).toBe('');
});
it('should not allow expiration date action when node has no update permission', () => {
node.entry.properties['qshare:sharedId'] = 'sharedId';
node.entry.allowableOperations = [];
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input[formcontrolname="time"]').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('.mat-slide-toggle[data-automation-id="adf-expire-toggle"]')
.classList).toContain('mat-disabled');
});
it('should update node expiration date with selected date', () => {
const date = moment();
node.entry.allowableOperations = ['update'];
node.entry.properties['qshare:sharedId'] = 'sharedId';
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
fixture.componentInstance.form.controls['time'].setValue(null);
component.data = {
node,
baseShareUrl: 'some-url/'
};
fixture.detectChanges();
fixture.nativeElement
.querySelector(
'mat-slide-toggle[data-automation-id="adf-expire-toggle"] label'
)
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date);
fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.utc().format() }
});
});
describe('datetimepicker type', () => {
beforeEach(() => {
spyOn(nodesApiService, 'updateNode').and.returnValue(of({}));
spyOn(sharedLinksApiService, 'createSharedLinks').and.returnValue(of({}));
node.entry.allowableOperations = ['update'];
component.data = {
node,
baseShareUrl: 'some-url/'
};
});
it('it should update node with input date and end of day time when type is `date`', () => {
const dateTimePickerType = 'date';
const date = moment('2525-01-01 13:00:00');
spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType);
fixture.detectChanges();
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date);
fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.endOf('day').utc().format() }
});
});
it('it should update node with input date and time when type is `datetime`', () => {
const dateTimePickerType = 'datetime';
const date = moment('2525-01-01 13:00:00');
spyOn(appConfigService, 'get').and.callFake(() => dateTimePickerType);
fixture.detectChanges();
fixture.nativeElement.querySelector('mat-slide-toggle[data-automation-id="adf-expire-toggle"] label')
.dispatchEvent(new MouseEvent('click'));
fixture.componentInstance.form.controls['time'].setValue(date);
fixture.detectChanges();
expect(nodesApiService.updateNode).toHaveBeenCalledWith('nodeId', {
properties: { 'qshare:expiryDate': date.utc().format() }
});
});
});
});

View File

@@ -0,0 +1,299 @@
/*!
* @license
* Copyright 2019 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,
Inject,
OnInit,
ViewEncapsulation,
ViewChild,
OnDestroy
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialog, MatSlideToggleChange } from '@angular/material';
import { FormGroup, FormControl } from '@angular/forms';
import { Observable, throwError, Subject } from 'rxjs';
import {
skip,
distinctUntilChanged,
mergeMap,
catchError,
takeUntil
} from 'rxjs/operators';
import {
SharedLinksApiService,
NodesApiService,
ContentService,
RenditionsService,
AppConfigService
} from '@alfresco/adf-core';
import { SharedLinkEntry, Node } from '@alfresco/js-api';
import { ConfirmDialogComponent } from '../dialogs/confirm.dialog';
import moment from 'moment-es6';
import { ContentNodeShareSettings } from './content-node-share.settings';
@Component({
selector: 'adf-share-dialog',
templateUrl: './content-node-share.dialog.html',
styleUrls: ['./content-node-share.dialog.scss'],
host: { class: 'adf-share-dialog' },
encapsulation: ViewEncapsulation.None
})
export class ShareDialogComponent implements OnInit, OnDestroy {
minDate = moment().add(1, 'd');
sharedId: string;
fileName: string;
baseShareUrl: string;
isFileShared: boolean = false;
isDisabled: boolean = false;
form: FormGroup = new FormGroup({
sharedUrl: new FormControl(''),
time: new FormControl({ value: '', disabled: false })
});
type = 'datetime';
@ViewChild('matDatetimepickerToggle')
matDatetimepickerToggle;
@ViewChild('slideToggleExpirationDate')
slideToggleExpirationDate;
@ViewChild('dateTimePickerInput')
dateTimePickerInput;
private onDestroy$ = new Subject<boolean>();
constructor(
private appConfigService: AppConfigService,
private sharedLinksApiService: SharedLinksApiService,
private dialogRef: MatDialogRef<ShareDialogComponent>,
private dialog: MatDialog,
private nodesApiService: NodesApiService,
private contentService: ContentService,
private renditionService: RenditionsService,
@Inject(MAT_DIALOG_DATA) public data: ContentNodeShareSettings
) {}
ngOnInit() {
this.type = this.appConfigService.get<string>('sharedLinkDateTimePickerType', 'datetime');
if (!this.canUpdate) {
this.form.controls['time'].disable();
}
this.form.controls.time.valueChanges
.pipe(
skip(1),
distinctUntilChanged(),
mergeMap(
(updates) => this.updateNode(updates),
(formUpdates) => formUpdates
),
catchError((error) => {
return throwError(error);
}),
takeUntil(this.onDestroy$)
)
.subscribe(updates => this.updateEntryExpiryDate(updates));
if (this.data.node && this.data.node.entry) {
this.fileName = this.data.node.entry.name;
this.baseShareUrl = this.data.baseShareUrl;
const properties = this.data.node.entry.properties;
if (!properties || !properties['qshare:sharedId']) {
this.createSharedLinks(this.data.node.entry.id);
} else {
this.sharedId = properties['qshare:sharedId'];
this.isFileShared = true;
this.updateForm();
}
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
removeShare() {
this.deleteSharedLink(this.sharedId);
}
onSlideShareChange(event: any) {
if (event.checked) {
this.createSharedLinks(this.data.node.entry.id);
} else {
this.openConfirmationDialog();
}
}
get canUpdate() {
const { entry } = this.data.node;
if (entry && entry.allowableOperations) {
return this.contentService.hasAllowableOperations(entry, 'update');
}
return true;
}
onToggleExpirationDate(slideToggle: MatSlideToggleChange) {
if (slideToggle.checked) {
this.matDatetimepickerToggle.datetimepicker.open();
} else {
this.matDatetimepickerToggle.datetimepicker.close();
this.form.controls.time.setValue(null);
}
}
onDatetimepickerClosed() {
this.dateTimePickerInput.nativeElement.blur();
if (!this.form.controls.time.value) {
this.slideToggleExpirationDate.checked = false;
}
}
private openConfirmationDialog() {
this.isFileShared = false;
this.dialog
.open(ConfirmDialogComponent, {
data: {
title: 'SHARE.CONFIRMATION.DIALOG-TITLE',
message: 'SHARE.CONFIRMATION.MESSAGE',
yesLabel: 'SHARE.CONFIRMATION.REMOVE',
noLabel: 'SHARE.CONFIRMATION.CANCEL'
},
minWidth: '250px',
closeOnNavigation: true
})
.beforeClose()
.subscribe((deleteSharedLink) => {
if (deleteSharedLink) {
this.deleteSharedLink(this.sharedId);
} else {
this.isFileShared = true;
}
});
}
private createSharedLinks(nodeId: string) {
this.isDisabled = true;
this.sharedLinksApiService.createSharedLinks(nodeId).subscribe(
(sharedLink: SharedLinkEntry) => {
if (sharedLink.entry) {
this.sharedId = sharedLink.entry.id;
if (this.data.node.entry.properties) {
this.data.node.entry.properties['qshare:sharedId'] = this.sharedId;
} else {
this.data.node.entry.properties = {
'qshare:sharedId': this.sharedId
};
}
this.isDisabled = false;
this.isFileShared = true;
this.renditionService
.generateRenditionForNode(this.data.node.entry.id)
.subscribe(() => {});
this.updateForm();
}
},
() => {
this.isDisabled = false;
this.isFileShared = false;
}
);
}
deleteSharedLink(sharedId: string) {
this.isDisabled = true;
this.sharedLinksApiService
.deleteSharedLink(sharedId)
.subscribe((response: any) => {
if (response instanceof Error) {
this.isDisabled = false;
this.isFileShared = true;
this.handleError(response);
} else {
if (this.data.node.entry.properties) {
this.data.node.entry.properties['qshare:sharedId'] = null;
this.data.node.entry.properties['qshare:expiryDate'] = null;
}
this.dialogRef.close(false);
}
}
);
}
private handleError(error: Error) {
let message = 'SHARE.UNSHARE_ERROR';
let statusCode = 0;
try {
statusCode = JSON.parse(error.message).error.statusCode;
} catch {}
if (statusCode === 403) {
message = 'SHARE.UNSHARE_PERMISSION_ERROR';
}
this.sharedLinksApiService.error.next({
statusCode,
message
});
}
private updateForm() {
const { entry } = this.data.node;
let expiryDate = null;
if (entry && entry.properties) {
expiryDate = entry.properties['qshare:expiryDate'];
}
this.form.setValue({
sharedUrl: `${this.baseShareUrl}${this.sharedId}`,
time: expiryDate ? moment(expiryDate).local() : null
});
}
private updateNode(date: moment.Moment): Observable<Node> {
return this.nodesApiService.updateNode(this.data.node.entry.id, {
properties: {
'qshare:expiryDate': date ?
(this.type === 'date' ? date.endOf('day').utc().format() : date.utc().format()) :
null
}
});
}
private updateEntryExpiryDate(date: moment.Moment) {
const { properties } = this.data.node.entry;
if (properties) {
properties['qshare:expiryDate'] = date
? date.local()
: null;
}
}
}

View File

@@ -0,0 +1,150 @@
/*!
* @license
* Copyright 2019 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ComponentFixture } from '@angular/core/testing';
import { Component } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ContentTestingModule } from '../testing/content.testing.module';
import { setupTestBed, CoreModule, SharedLinksApiService } from '@alfresco/adf-core';
import { ContentNodeShareModule } from './content-node-share.module';
@Component({
selector: 'adf-node-share-test-component',
template: `
<button
[disabled]="!shareRef.isFile"
#shareRef="adfShare"
[baseShareUrl]="baseShareUrl"
[adf-share]="documentList.selection[0]"
[title]="shareRef.isShared ? 'Shared' : 'Not Shared'">
</button>
`
})
class NodeShareTestComponent {
baseShareUrl = 'some-url/';
documentList = {
selection: []
};
}
describe('NodeSharedDirective', () => {
let fixture: ComponentFixture<NodeShareTestComponent>;
let component: NodeShareTestComponent;
let document: any;
let shareButtonElement;
let selection;
setupTestBed({
imports: [
NoopAnimationsModule,
CoreModule.forRoot(),
ContentTestingModule,
ContentNodeShareModule
],
declarations: [
NodeShareTestComponent
],
providers: [
SharedLinksApiService
]
});
beforeEach(() => {
fixture = TestBed.createComponent(NodeShareTestComponent);
document = TestBed.get(DOCUMENT);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
if (fixture) {
fixture.destroy();
}
});
beforeEach(() => {
shareButtonElement = fixture.debugElement.query(By.css('button')).nativeElement;
selection = {
entry: {
isFile: true,
properties: {}
}
};
});
it('should have share button disabled when selection is empty', async () => {
fixture.whenStable();
fixture.detectChanges();
expect(shareButtonElement.disabled).toBe(true);
});
it('should have button enabled when selection is not empty', async () => {
component.documentList.selection = [selection];
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
expect(shareButtonElement.disabled).toBe(false);
});
it('should have button disabled when selection is not a file', async () => {
selection.entry.isFile = false;
component.documentList.selection = [selection];
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
expect(shareButtonElement.disabled).toBe(true);
});
it('should indicate if file is not shared', async () => {
component.documentList.selection = [selection];
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
expect(shareButtonElement.title).toBe('Not Shared');
});
it('should indicate if file is already shared', async () => {
selection.entry.properties['qshare:sharedId'] = 'someId';
component.documentList.selection = [selection];
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
expect(shareButtonElement.title).toBe('Shared');
});
it('should open share dialog on click event', async () => {
selection.entry.properties['qshare:sharedId'] = 'someId';
component.documentList.selection = [selection];
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
shareButtonElement.click();
fixture.detectChanges();
expect(document.querySelector('.adf-share-link-dialog')).not.toBe(null);
});
});

View File

@@ -0,0 +1,109 @@
/*!
* @license
* Copyright 2019 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 { Directive, Input, HostListener, OnChanges, NgZone, OnDestroy } from '@angular/core';
import { MatDialog } from '@angular/material';
import { NodeEntry, Node } from '@alfresco/js-api';
import { ShareDialogComponent } from './content-node-share.dialog';
import { Observable, from, Subject } from 'rxjs';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { takeUntil } from 'rxjs/operators';
@Directive({
selector: '[adf-share]',
exportAs: 'adfShare'
})
export class NodeSharedDirective implements OnChanges, OnDestroy {
isFile: boolean = false;
isShared: boolean = false;
/** Node to share. */
// tslint:disable-next-line:no-input-rename
@Input('adf-share')
node: NodeEntry;
/** Prefix to add to the generated link. */
@Input()
baseShareUrl: string;
private onDestroy$ = new Subject<boolean>();
constructor(
private dialog: MatDialog,
private zone: NgZone,
private alfrescoApiService: AlfrescoApiService) {
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
shareNode(nodeEntry: NodeEntry) {
if (nodeEntry && nodeEntry.entry && nodeEntry.entry.isFile) {
// shared and favorite
const nodeId = nodeEntry.entry['nodeId'] || nodeEntry.entry['guid'];
if (nodeId) {
this.getNodeInfo(nodeId).subscribe((entry) => {
this.openShareLinkDialog({ entry });
});
} else {
this.openShareLinkDialog(nodeEntry);
}
}
}
private getNodeInfo(nodeId: string): Observable<Node> {
const options = {
include: ['allowableOperations']
};
return from(this.alfrescoApiService.nodesApi.getNodeInfo(nodeId, options));
}
private openShareLinkDialog(node: NodeEntry) {
this.dialog.open(ShareDialogComponent, {
width: '600px',
panelClass: 'adf-share-link-dialog',
data: {
node,
baseShareUrl: this.baseShareUrl
}
});
}
ngOnChanges() {
this.zone.onStable
.pipe(takeUntil(this.onDestroy$))
.subscribe(() => {
if (this.node && this.node.entry) {
this.isFile = this.node.entry.isFile;
this.isShared = this.node.entry.properties ? this.node.entry.properties['qshare:sharedId'] : false;
}
});
}
@HostListener('click')
onClick() {
if (this.node) {
this.shareNode(this.node);
}
}
}

View File

@@ -0,0 +1,55 @@
/*!
* @license
* Copyright 2019 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 { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CoreModule } from '@alfresco/adf-core';
import { MaterialModule } from '../material.module';
import { ShareDialogComponent } from './content-node-share.dialog';
import { NodeSharedDirective } from './content-node-share.directive';
@NgModule({
imports: [
CoreModule,
CommonModule,
MaterialModule
],
declarations: [
ShareDialogComponent,
NodeSharedDirective
],
exports: [
ShareDialogComponent,
NodeSharedDirective
],
entryComponents: [
ShareDialogComponent
]
})
export class ContentNodeShareModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ContentNodeShareModule
};
}
static forChild(): ModuleWithProviders {
return {
ngModule: ContentNodeShareModule
};
}
}

View File

@@ -0,0 +1,23 @@
/*!
* @license
* Copyright 2019 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 { NodeEntry } from '@alfresco/js-api';
export interface ContentNodeShareSettings {
baseShareUrl: string;
node: NodeEntry;
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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';

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 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 './content-node-share.settings';
export * from './content-node-share.dialog';
export * from './content-node-share.directive';
export * from './content-node-share.module';

View File

@@ -0,0 +1,167 @@
/*!
* @license
* Copyright 2019 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, ModuleWithProviders } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CoreModule, TRANSLATION_PROVIDER } from '@alfresco/adf-core';
import { MaterialModule } from './material.module';
import { SocialModule } from './social/social.module';
import { TagModule } from './tag/tag.module';
import { WebScriptModule } from './webscript/webscript.module';
import { DocumentListModule } from './document-list/document-list.module';
import { UploadModule } from './upload/upload.module';
import { SearchModule } from './search/search.module';
import { SitesDropdownModule } from './site-dropdown/sites-dropdown.module';
import { BreadcrumbModule } from './breadcrumb/breadcrumb.module';
import { VersionManagerModule } from './version-manager/version-manager.module';
import { ContentNodeSelectorModule } from './content-node-selector/content-node-selector.module';
import { ContentNodeShareModule } from './content-node-share/content-node-share.module';
import { ContentDirectiveModule } from './directives/content-directive.module';
import { DialogModule } from './dialogs/dialog.module';
import { FolderDirectiveModule } from './folder-directive/folder-directive.module';
import { ContentMetadataModule } from './content-metadata/content-metadata.module';
import { PermissionManagerModule } from './permission-manager/permission-manager.module';
import { TreeViewModule } from './tree-view/tree-view.module';
@NgModule({
imports: [
CoreModule,
SocialModule,
TagModule,
CommonModule,
WebScriptModule,
FormsModule,
ReactiveFormsModule,
DialogModule,
SearchModule,
DocumentListModule,
UploadModule,
MaterialModule,
SitesDropdownModule,
BreadcrumbModule,
ContentNodeSelectorModule,
ContentNodeShareModule,
ContentMetadataModule,
FolderDirectiveModule,
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
],
exports: [
SocialModule,
TagModule,
WebScriptModule,
DocumentListModule,
UploadModule,
SearchModule,
SitesDropdownModule,
BreadcrumbModule,
ContentNodeSelectorModule,
ContentNodeShareModule,
ContentMetadataModule,
DialogModule,
FolderDirectiveModule,
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
]
})
export class ContentModuleLazy {}
@NgModule({
imports: [
CoreModule,
SocialModule,
TagModule,
CommonModule,
WebScriptModule,
FormsModule,
ReactiveFormsModule,
DialogModule,
SearchModule,
DocumentListModule,
UploadModule,
MaterialModule,
SitesDropdownModule,
BreadcrumbModule,
ContentNodeSelectorModule,
ContentNodeShareModule,
ContentMetadataModule,
FolderDirectiveModule,
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
],
providers: [
{
provide: TRANSLATION_PROVIDER,
multi: true,
useValue: {
name: 'adf-content-services',
source: 'assets/adf-content-services'
}
}
],
exports: [
SocialModule,
TagModule,
WebScriptModule,
DocumentListModule,
UploadModule,
SearchModule,
SitesDropdownModule,
BreadcrumbModule,
ContentNodeSelectorModule,
ContentNodeShareModule,
ContentMetadataModule,
DialogModule,
FolderDirectiveModule,
ContentDirectiveModule,
PermissionManagerModule,
VersionManagerModule,
TreeViewModule
]
})
export class ContentModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: ContentModule,
providers: [
{
provide: TRANSLATION_PROVIDER,
multi: true,
useValue: {
name: 'adf-content-services',
source: 'assets/adf-content-services'
}
}
]
};
}
static forChild(): ModuleWithProviders {
return {
ngModule: ContentModuleLazy
};
}
}

View File

@@ -0,0 +1,18 @@
<h1 mat-dialog-title data-automation-id="adf-confirm-dialog-title">{{ title | translate }}</h1>
<mat-dialog-content>
<p *ngIf="!htmlContent; else cutomContent" data-automation-id="adf-confirm-dialog-base-message">
{{ message | translate }}
</p>
<ng-template #cutomContent>
<span [innerHTML]="sanitizedHtmlContent()" data-automation-id="adf-confirm-dialog-custom-content">
</span>
</ng-template>
</mat-dialog-content>
<mat-dialog-actions>
<span class="adf-dialog-spacer" data-automation-id="adf-confirm-dialog-spacer"></span>
<button id="adf-confirm-accept" mat-button color="primary" data-automation-id="adf-confirm-dialog-confirmation"
[mat-dialog-close]="true">{{ yesLabel | translate }}</button>
<button id="adf-confirm-all" mat-button *ngIf="thirdOptionLabel" [mat-dialog-close]="thirdOptionLabel" data-automation-id="adf-confirm-dialog-confirm-all">{{ thirdOptionLabel | translate }}</button>
<button id="adf-confirm-cancel" mat-button [mat-dialog-close]="false" data-automation-id="adf-confirm-dialog-reject"
cdkFocusInitial>{{ noLabel | translate }}</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,7 @@
.adf-dialog-spacer {
flex: 1 1 auto;
}
.adf-confirm-dialog .mat-dialog-actions .mat-button-wrapper {
text-transform: uppercase;
}

View File

@@ -0,0 +1,167 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { ComponentFixture } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { setupTestBed } from '@alfresco/adf-core';
import { ConfirmDialogComponent } from './confirm.dialog';
import { ContentTestingModule } from '../testing/content.testing.module';
import { By } from '@angular/platform-browser';
describe('Confirm Dialog Component', () => {
let fixture: ComponentFixture<ConfirmDialogComponent>;
let component: ConfirmDialogComponent;
const dialogRef = {
close: jasmine.createSpy('close')
};
const data = {
title: 'Fake Title',
message: 'Base Message',
yesLabel: 'TAKE THIS',
noLabel: 'MAYBE NO'
};
setupTestBed({
imports: [ContentTestingModule],
providers: [
{ provide: MatDialogRef, useValue: dialogRef },
{ provide: MAT_DIALOG_DATA, useValue: data }
]
});
beforeEach(() => {
dialogRef.close.calls.reset();
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
});
afterEach(() => {
fixture.destroy();
});
describe('When no html is given', () => {
beforeEach(() => {
fixture.detectChanges();
});
it('should init form with folder name and description', () => {
expect(component.title).toBe('Fake Title');
expect(component.message).toBe('Base Message');
expect(component.yesLabel).toBe('TAKE THIS');
expect(component.noLabel).toBe('MAYBE NO');
});
it('should render the title', () => {
const titleElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-title"]')
);
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Fake Title');
});
it('should render the message', () => {
const messageElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-base-message"]')
);
expect(messageElement).not.toBeNull();
expect(messageElement.nativeElement.innerText).toBe('Base Message');
});
it('should render the YES label', () => {
const messageElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-confirmation"]')
);
expect(messageElement).not.toBeNull();
expect(messageElement.nativeElement.innerText).toBe('TAKE THIS');
});
it('should render the NO label', () => {
const messageElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-reject"]')
);
expect(messageElement).not.toBeNull();
expect(messageElement.nativeElement.innerText).toBe('MAYBE NO');
});
});
describe('When custom html is given', () => {
beforeEach(() => {
component.htmlContent = `<div> I am about to do to you what Limp Bizkit did to music in the late 90s.</div>`;
fixture.detectChanges();
});
it('should render the title', () => {
const titleElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-title"]')
);
expect(titleElement).not.toBeNull();
expect(titleElement.nativeElement.innerText).toBe('Fake Title');
});
it('should render the custom html', () => {
const customElement = fixture.nativeElement.querySelector(
'[data-automation-id="adf-confirm-dialog-custom-content"] div'
);
expect(customElement).not.toBeNull();
expect(customElement.innerText).toBe(
'I am about to do to you what Limp Bizkit did to music in the late 90s.'
);
});
it('should render the YES label', () => {
const messageElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-confirmation"]')
);
expect(messageElement).not.toBeNull();
expect(messageElement.nativeElement.innerText).toBe('TAKE THIS');
});
it('should render the NO label', () => {
const messageElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-reject"]')
);
expect(messageElement).not.toBeNull();
expect(messageElement.nativeElement.innerText).toBe('MAYBE NO');
});
});
describe('thirdOptionLabel is given', () => {
it('should NOT render the thirdOption if is thirdOptionLabel is not passed', () => {
component.thirdOptionLabel = undefined;
fixture.detectChanges();
const thirdOptionElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-confirm-all"]')
);
expect(thirdOptionElement).toBeFalsy();
});
it('should render the thirdOption if thirdOptionLabel is passed', () => {
component.thirdOptionLabel = 'Yes All';
fixture.detectChanges();
const thirdOptionElement = fixture.debugElement.query(
By.css('[data-automation-id="adf-confirm-dialog-confirm-all"]')
);
expect(thirdOptionElement).not.toBeNull();
expect(thirdOptionElement.nativeElement.innerText).toBe('YES ALL');
});
});
});

View File

@@ -0,0 +1,61 @@
/*!
* @license
* Copyright 2019 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, Inject, ViewEncapsulation, SecurityContext } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material';
import { DomSanitizer } from '@angular/platform-browser';
export interface ConfirmDialogComponentProps {
title?: string;
message?: string;
yesLabel?: string;
thirdOptionLabel?: string;
noLabel?: string;
htmlContent?: string;
}
@Component({
selector: 'adf-confirm-dialog',
templateUrl: './confirm.dialog.html',
styleUrls: ['./confirm.dialog.scss'],
host: { 'class': 'adf-confirm-dialog' },
encapsulation: ViewEncapsulation.None
})
export class ConfirmDialogComponent {
title: string;
message: string;
yesLabel: string;
noLabel: string;
thirdOptionLabel: string;
htmlContent: string;
constructor(@Inject(MAT_DIALOG_DATA) data: ConfirmDialogComponentProps, private sanitizer: DomSanitizer) {
data = data || {};
this.title = data.title || 'ADF_CONFIRM_DIALOG.CONFIRM';
this.message = data.message || 'ADF_CONFIRM_DIALOG.MESSAGE';
this.yesLabel = data.yesLabel || 'ADF_CONFIRM_DIALOG.YES_LABEL';
this.thirdOptionLabel = data.thirdOptionLabel;
this.noLabel = data.noLabel || 'ADF_CONFIRM_DIALOG.NO_LABEL';
this.htmlContent = data.htmlContent;
}
sanitizedHtmlContent(): string {
return this.sanitizer.sanitize(SecurityContext.HTML, this.htmlContent);
}
}

View File

@@ -0,0 +1,60 @@
/*!
* @license
* Copyright 2019 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 { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CoreModule } from '@alfresco/adf-core';
import { MaterialModule } from '../material.module';
import { FolderDialogComponent } from './folder.dialog';
import { NodeLockDialogComponent } from './node-lock.dialog';
import { ConfirmDialogComponent } from './confirm.dialog';
import { MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { MatMomentDatetimeModule } from '@mat-datetimepicker/moment';
import { LibraryDialogComponent } from './library/library.dialog';
@NgModule({
imports: [
CommonModule,
MaterialModule,
CoreModule,
FormsModule,
ReactiveFormsModule,
MatMomentDatetimeModule,
MatDatetimepickerModule
],
declarations: [
FolderDialogComponent,
NodeLockDialogComponent,
ConfirmDialogComponent,
LibraryDialogComponent
],
exports: [
FolderDialogComponent,
NodeLockDialogComponent,
ConfirmDialogComponent,
LibraryDialogComponent
],
entryComponents: [
FolderDialogComponent,
NodeLockDialogComponent,
ConfirmDialogComponent,
LibraryDialogComponent
]
})
export class DialogModule {}

View File

@@ -0,0 +1,45 @@
/*!
* @license
* Copyright 2019 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 { FormControl } from '@angular/forms';
const I18N_ERRORS_PATH = 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS';
export function forbidSpecialCharacters({ value }: FormControl) {
const specialCharacters: RegExp = /([\*\"\<\>\\\/\?\:\|])/;
const isValid: boolean = !specialCharacters.test(value);
return (isValid) ? null : {
message: `${I18N_ERRORS_PATH}.SPECIAL_CHARACTERS`
};
}
export function forbidEndingDot({ value }: FormControl) {
const isValid: boolean = ((value || '').trim().split('').pop() !== '.');
return isValid ? null : {
message: `${I18N_ERRORS_PATH}.ENDING_DOT`
};
}
export function forbidOnlySpaces({ value }: FormControl) {
const isValid: boolean = !!((value || '')).trim();
return isValid ? null : {
message: `${I18N_ERRORS_PATH}.ONLY_SPACES`
};
}

View File

@@ -0,0 +1,62 @@
<h2 mat-dialog-title>
{{ (editing ? editTitle : createTitle) | translate }}
</h2>
<mat-dialog-content>
<form [formGroup]="form" (submit)="submit()">
<mat-form-field class="adf-full-width">
<input
id="adf-folder-name-input"
placeholder="{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.LABEL' | translate }}"
matInput
required
[formControl]="form.controls['name']"/>
<mat-hint *ngIf="form.controls['name'].dirty">
<span *ngIf="form.controls['name'].errors?.required">
{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
</span>
<span *ngIf="!form.controls['name'].errors?.required && form.controls['name'].errors?.message">
{{ form.controls['name'].errors?.message | translate }}
</span>
</mat-hint>
</mat-form-field>
<br />
<br />
<mat-form-field class="adf-full-width">
<textarea
id="adf-folder-description-input"
matInput
placeholder="{{ 'CORE.FOLDER_DIALOG.FOLDER_DESCRIPTION.LABEL' | translate }}"
rows="4"
[formControl]="form.controls['description']"></textarea>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions class="adf-dialog-buttons">
<span class="adf-fill-remaining-space"></span>
<button
mat-button
id="adf-folder-cancel-button"
mat-dialog-close>
{{ 'CORE.FOLDER_DIALOG.CANCEL_BUTTON.LABEL' | translate }}
</button>
<button class="adf-dialog-action-button"
id="adf-folder-create-button"
mat-button
(click)="submit()"
[disabled]="!form.valid">
{{
(editing
? 'CORE.FOLDER_DIALOG.UPDATE_BUTTON.LABEL'
: 'CORE.FOLDER_DIALOG.CREATE_BUTTON.LABEL'
) | translate
}}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,36 @@
.adf-fill-remaining-space {
flex: 1 1 auto;
}
.adf-full-width {
width: 100%;
}
.adf-lock-file-name {
.mat-checkbox-layout {
width: 100%;
}
.mat-checkbox-label {
text-overflow: ellipsis;
overflow: hidden;
}
.mat-checkbox-inner-container {
margin: auto 0;
margin-right: 8px;
}
}
@mixin adf-dialog-theme($theme) {
$primary: map-get($theme, primary);
.adf-dialog-buttons button {
text-transform: uppercase;
}
.adf-dialog-action-button:enabled {
color: mat-color($primary);
}
}

View File

@@ -0,0 +1,308 @@
/*!
* @license
* Copyright 2019 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 { TestBed } from '@angular/core/testing';
import { async, ComponentFixture } from '@angular/core/testing';
import { MatDialogRef } from '@angular/material';
import { NodesApiService, setupTestBed } from '@alfresco/adf-core';
import { FolderDialogComponent } from './folder.dialog';
import { of, throwError } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { By } from '@angular/platform-browser';
describe('FolderDialogComponent', () => {
let fixture: ComponentFixture<FolderDialogComponent>;
let component: FolderDialogComponent;
let nodesApi: NodesApiService;
const dialogRef = {
close: jasmine.createSpy('close')
};
setupTestBed({
imports: [ContentTestingModule],
providers: [
{ provide: MatDialogRef, useValue: dialogRef }
]
});
beforeEach(() => {
dialogRef.close.calls.reset();
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.get(NodesApiService);
});
afterEach(() => {
fixture.destroy();
});
describe('Edit', () => {
beforeEach(() => {
component.data = {
folder: {
id: 'node-id',
name: 'folder-name',
properties: {
['cm:description']: 'folder-description'
}
}
};
fixture.detectChanges();
});
it('should init form with folder name and description', () => {
expect(component.name).toBe('folder-name');
expect(component.description).toBe('folder-description');
});
it('should have the proper title', () => {
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.updateNode).toHaveBeenCalledWith(
'node-id',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
}
}
);
});
it('should call dialog to close with form data when submit is successfully', () => {
const folder = {
data: 'folder-data'
};
spyOn(nodesApi, 'updateNode').and.returnValue(of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should emit success output event with folder when submit is successful', async(() => {
const folder = { data: 'folder-data' };
let expectedNode = null;
spyOn(nodesApi, 'updateNode').and.returnValue(of(folder));
component.success.subscribe((node) => { expectedNode = node; });
component.submit();
fixture.whenStable().then(() => {
expect(expectedNode).toBe(folder);
});
}));
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'updateNode');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.updateNode).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(throwError('error'));
spyOn(component, 'handleError').and.callFake((val) => val);
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
});
describe('Create', () => {
beforeEach(() => {
component.data = {
parentNodeId: 'parentNodeId',
folder: null
};
fixture.detectChanges();
});
it('should have the proper title', () => {
const title = fixture.debugElement.query(By.css('[mat-dialog-title]'));
expect(title === null).toBe(false);
expect(title.nativeElement.innerText.trim()).toBe('CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE');
});
it('should init form with empty inputs', () => {
expect(component.name).toBe('');
expect(component.description).toBe('');
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith(
'parentNodeId',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:folder'
}
);
});
it('should submit updated values if form is valid (with custom nodeType)', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(of({}));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
component.nodeType = 'cm:sushi';
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith(
'parentNodeId',
{
name: 'folder-name-update',
properties: {
'cm:title': 'folder-name-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:sushi'
}
);
});
it('should call dialog to close with form data when submit is successfully', () => {
const folder = {
data: 'folder-data'
};
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
spyOn(nodesApi, 'createFolder').and.returnValue(of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'createFolder');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.createFolder).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(throwError('error'));
spyOn(component, 'handleError').and.callFake((val) => val);
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
expect(component.handleError).toHaveBeenCalled();
expect(dialogRef.close).not.toHaveBeenCalled();
});
describe('Error events ', () => {
it('should raise error for 409', (done) => {
const error = {
message: '{ "error": { "statusCode" : 409 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.EXISTENT_FOLDER');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(throwError(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
it('should raise generic error', (done) => {
const error = {
message: '{ "error": { "statusCode" : 123 } }'
};
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.GENERIC');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(throwError(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
component.submit();
});
});
});
});

View File

@@ -0,0 +1,162 @@
/*!
* @license
* Copyright 2019 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 { Observable } from 'rxjs';
import { Component, Inject, OnInit, Optional, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Node } from '@alfresco/js-api';
import { NodesApiService, TranslationService } from '@alfresco/adf-core';
import { forbidEndingDot, forbidOnlySpaces, forbidSpecialCharacters } from './folder-name.validators';
@Component({
selector: 'adf-folder-dialog',
styleUrls: ['./folder.dialog.scss'],
templateUrl: './folder.dialog.html'
})
export class FolderDialogComponent implements OnInit {
form: FormGroup;
folder: Node = null;
/** Emitted when the edit/create folder give error for example a folder with same name already exist
*/
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the edit/create folder is successfully created/modified
*/
@Output()
success: EventEmitter<any> = new EventEmitter<Node>();
editTitle = 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE';
createTitle = 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE';
nodeType = 'cm:folder';
constructor(
private formBuilder: FormBuilder,
private dialog: MatDialogRef<FolderDialogComponent>,
private nodesApi: NodesApiService,
private translation: TranslationService,
@Optional()
@Inject(MAT_DIALOG_DATA)
public data: any
) {
if (data) {
this.editTitle = data.editTitle || this.editTitle;
this.createTitle = data.createTitle || this.createTitle;
this.nodeType = data.nodeType || this.nodeType;
}
}
get editing(): boolean {
return !!this.data.folder;
}
ngOnInit() {
const { folder } = this.data;
let name = '';
let description = '';
if (folder) {
const { properties } = folder;
name = folder.name || '';
description = properties ? properties['cm:description'] : '';
}
const validators = {
name: [
Validators.required,
forbidSpecialCharacters,
forbidEndingDot,
forbidOnlySpaces
]
};
this.form = this.formBuilder.group({
name: [ name, validators.name ],
description: [ description ]
});
}
get name(): string {
const { name } = this.form.value;
return (name || '').trim();
}
get description(): string {
const { description } = this.form.value;
return (description || '').trim();
}
private get properties(): any {
const { name: title, description } = this;
return {
'cm:title': title,
'cm:description': description
};
}
private create(): Observable<Node> {
const { name, properties, nodeType, nodesApi, data: { parentNodeId} } = this;
return nodesApi.createFolder(parentNodeId, { name, properties, nodeType });
}
private edit(): Observable<Node> {
const { name, properties, nodesApi, data: { folder: { id: nodeId }} } = this;
return nodesApi.updateNode(nodeId, { name, properties });
}
submit() {
const { form, dialog, editing } = this;
if (!form.valid) { return; }
(editing ? this.edit() : this.create())
.subscribe(
(folder: Node) => {
this.success.emit(folder);
dialog.close(folder);
},
(error) => this.handleError(error)
);
}
handleError(error: any): any {
let errorMessage = 'CORE.MESSAGES.ERRORS.GENERIC';
try {
const { error: { statusCode } } = JSON.parse(error.message);
if (statusCode === 409) {
errorMessage = 'CORE.MESSAGES.ERRORS.EXISTENT_FOLDER';
}
} catch (err) { /* Do nothing, keep the original message */ }
this.error.emit(this.translation.instant(errorMessage));
return error;
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @license
* Copyright 2019 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';

View File

@@ -0,0 +1,95 @@
<h2 mat-dialog-title>{{ createTitle | translate }}</h2>
<mat-dialog-content>
<form novalidate [formGroup]="form" (submit)="submit()">
<mat-form-field>
<input
placeholder="{{ 'LIBRARY.DIALOG.FORM.NAME' | translate }}"
required
matInput
autofocus
formControlName="title"
autocomplete="off"
/>
<mat-hint *ngIf="libraryTitleExists">{{
'LIBRARY.HINTS.SITE_TITLE_EXISTS' | translate
}}</mat-hint>
<mat-error *ngIf="form.controls['title'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.TITLE_TOO_LONG' | translate }}
</mat-error>
<mat-error *ngIf="form.controls['title'].hasError('minlength')">
{{ 'LIBRARY.ERRORS.TITLE_TOO_SHORT' | translate }}
</mat-error>
<mat-error *ngIf="form.controls['title'].errors?.message">
{{ form.controls['title'].errors?.message | translate }}
</mat-error>
</mat-form-field>
<mat-form-field>
<input
required
placeholder="{{ 'LIBRARY.DIALOG.FORM.SITE_ID' | translate }}"
matInput
formControlName="id"
autocomplete="off"
/>
<mat-error *ngIf="form.controls['id'].errors?.message">
{{ form.controls['id'].errors?.message | translate }}
</mat-error>
<mat-error *ngIf="form.controls['id'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.ID_TOO_LONG' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field>
<textarea
matInput
placeholder="{{ 'LIBRARY.DIALOG.FORM.DESCRIPTION' | translate }}"
rows="3"
formControlName="description"
></textarea>
<mat-error *ngIf="form.controls['description'].hasError('maxlength')">
{{ 'LIBRARY.ERRORS.DESCRIPTION_TOO_LONG' | translate }}
</mat-error>
</mat-form-field>
<mat-radio-group
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="visibilityOption"
(change)="visibilityChangeHandler($event)"
>
<mat-radio-button
color="primary"
[disabled]="option.disabled"
*ngFor="let option of visibilityOptions"
[attr.data-automation-id]="option.value"
[value]="option.value"
[checked]="visibilityOption.value === option.value"
>
{{ option.label | translate }}
</mat-radio-button>
</mat-radio-group>
</form>
</mat-dialog-content>
<mat-dialog-actions class="adf-action-buttons">
<button mat-button mat-dialog-close data-automation-id="cancel-library-id">
{{ 'LIBRARY.DIALOG.CANCEL' | translate }}
</button>
<button
color="primary"
mat-button
(click)="submit()"
[disabled]="!form.valid"
data-automation-id="create-library-id"
>
{{ 'LIBRARY.DIALOG.CREATE' | translate }}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,29 @@
.adf-library-dialog {
.mat-radio-group {
display: flex;
flex-direction: column;
margin: 0 0 20px;
}
.mat-radio-group .mat-radio-button {
margin: 10px 0;
}
.mat-form-field {
width: 100%;
}
mat-form-field {
padding-top: 20px;
}
.adf-action-buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
.mat-button {
text-transform: uppercase;
}
}
}

View File

@@ -0,0 +1,278 @@
/*!
* @license
* Copyright 2019 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 { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { CoreModule } from '@alfresco/adf-core';
import { LibraryDialogComponent } from './library.dialog';
import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MatDialogRef } from '@angular/material';
import {
AlfrescoApiService,
AlfrescoApiServiceMock,
setupTestBed
} from '@alfresco/adf-core';
describe('LibraryDialogComponent', () => {
let fixture;
let component;
let alfrescoApi;
let findSitesSpy;
const findSitesResponse = { list: { entries: [] } };
const dialogRef = {
close: jasmine.createSpy('close')
};
setupTestBed({
imports: [NoopAnimationsModule, CoreModule.forRoot(), ReactiveFormsModule],
declarations: [LibraryDialogComponent],
providers: [
{
provide: AlfrescoApiService,
useClass: AlfrescoApiServiceMock
},
{ provide: MatDialogRef, useValue: dialogRef }
],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
fixture = TestBed.createComponent(LibraryDialogComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
findSitesSpy = spyOn(alfrescoApi.getInstance().core.queriesApi, 'findSites');
});
afterEach(() => {
findSitesSpy.calls.reset();
});
it('should set library id automatically on title input', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('libraryTitle');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe('libraryTitle');
}));
it('should translate library title space character to dash for library id', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('library title');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe('library-title');
}));
it('should not change custom library id on title input', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.id.setValue('custom-id');
component.form.controls.id.markAsDirty();
tick(500);
flush();
fixture.detectChanges();
component.form.controls.title.setValue('library title');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe('custom-id');
}));
it('should invalidate form when library id already exists', fakeAsync(() => {
spyOn(alfrescoApi.sitesApi, 'getSite').and.returnValue(Promise.resolve());
fixture.detectChanges();
component.form.controls.id.setValue('existingLibrary');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.errors).toEqual({
message: 'LIBRARY.ERRORS.EXISTENT_SITE'
});
expect(component.form.valid).toBe(false);
}));
it('should create site when form is valid', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'createSite').and.returnValue(
Promise.resolve()
);
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('library title');
tick(500);
flush();
fixture.detectChanges();
component.submit();
fixture.detectChanges();
flush();
expect(alfrescoApi.sitesApi.createSite).toHaveBeenCalledWith({
id: 'library-title',
title: 'library title',
description: '',
visibility: 'PUBLIC'
});
}));
it('should not create site when form is invalid', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'createSite').and.returnValue(
Promise.resolve({})
);
spyOn(alfrescoApi.sitesApi, 'getSite').and.returnValue(Promise.resolve());
fixture.detectChanges();
component.form.controls.title.setValue('existingLibrary');
tick(500);
flush();
fixture.detectChanges();
component.submit();
fixture.detectChanges();
flush();
expect(alfrescoApi.sitesApi.createSite).not.toHaveBeenCalled();
}));
it('should notify when library title is already used', fakeAsync(() => {
spyOn(alfrescoApi.sitesApi, 'getSite').and.returnValue(Promise.resolve());
findSitesSpy.and.returnValue(Promise.resolve(
{ list: { entries: [{ entry: { title: 'TEST', id: 'library-id' } }] } }
));
fixture.detectChanges();
component.form.controls.title.setValue('test');
tick(500);
flush();
fixture.detectChanges();
expect(component.libraryTitleExists).toBe(true);
}));
it('should notify on 409 conflict error (might be in trash)', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
const error = { message: '{ "error": { "statusCode": 409 } }' };
spyOn(alfrescoApi.sitesApi, 'createSite').and.callFake(() => {
return Promise.reject(error);
});
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('test');
tick(500);
flush();
fixture.detectChanges();
component.submit();
fixture.detectChanges();
flush();
expect(component.form.controls.id.errors).toEqual({
message: 'LIBRARY.ERRORS.CONFLICT'
});
}));
it('should not translate library title if value is not a valid id', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('@@@####');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe(null);
}));
it('should translate library title partially for library id', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('@@@####library');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe('library');
}));
it('should translate library title multiple space character to one dash for library id', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('library title');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.id.value).toBe('library-title');
}));
it('should invalidate library title if is too short', fakeAsync(() => {
findSitesSpy.and.returnValue(Promise.resolve(findSitesResponse));
spyOn(alfrescoApi.sitesApi, 'getSite').and.callFake(() => {
return Promise.reject();
});
fixture.detectChanges();
component.form.controls.title.setValue('l');
tick(500);
flush();
fixture.detectChanges();
expect(component.form.controls.title.errors['minlength']).toBeTruthy();
expect(component.form.valid).toBe(false);
}));
});

View File

@@ -0,0 +1,280 @@
/*!
* @license
* Copyright 2019 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 { Observable, Subject, from } from 'rxjs';
import {
Component,
OnInit,
Output,
EventEmitter,
OnDestroy,
ViewEncapsulation
} from '@angular/core';
import {
FormBuilder,
FormGroup,
Validators,
FormControl,
AbstractControl
} from '@angular/forms';
import { MatDialogRef } from '@angular/material';
import { SiteBodyCreate, SiteEntry, SitePaging } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
import { debounceTime, mergeMap, takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-library-dialog',
styleUrls: ['./library.dialog.scss'],
templateUrl: './library.dialog.html',
encapsulation: ViewEncapsulation.None,
host: { class: 'adf-library-dialog' }
})
export class LibraryDialogComponent implements OnInit, OnDestroy {
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the new library is created successfully. The
* event parameter is a SiteEntry object with the details of the
* newly-created library.
*/
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
onDestroy$: Subject<boolean> = new Subject<boolean>();
createTitle = 'LIBRARY.DIALOG.CREATE_TITLE';
libraryTitleExists = false;
form: FormGroup;
visibilityOption: any;
visibilityOptions = [
{ value: 'PUBLIC', label: 'LIBRARY.VISIBILITY.PUBLIC', disabled: false },
{ value: 'PRIVATE', label: 'LIBRARY.VISIBILITY.PRIVATE', disabled: false },
{
value: 'MODERATED',
label: 'LIBRARY.VISIBILITY.MODERATED',
disabled: false
}
];
constructor(
private alfrescoApiService: AlfrescoApiService,
private formBuilder: FormBuilder,
private dialog: MatDialogRef<LibraryDialogComponent>
) {}
ngOnInit() {
const validators = {
id: [
Validators.required,
Validators.maxLength(72),
this.forbidSpecialCharacters
],
title: [
Validators.required,
this.forbidOnlySpaces,
Validators.minLength(2),
Validators.maxLength(256)
],
description: [Validators.maxLength(512)]
};
this.form = this.formBuilder.group({
title: [null, validators.title],
id: [null, validators.id, this.createSiteIdValidator()],
description: ['', validators.description]
});
this.visibilityOption = this.visibilityOptions[0].value;
this.form.controls['title'].valueChanges
.pipe(
debounceTime(300),
mergeMap(
(title) => this.checkLibraryNameExists(title),
(title) => title
),
takeUntil(this.onDestroy$)
)
.subscribe((title: string) => {
if (!this.form.controls['id'].dirty && this.canGenerateId(title)) {
this.form.patchValue({ id: this.sanitize(title.trim()) });
this.form.controls['id'].markAsTouched();
}
});
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
get title(): string {
const { title } = this.form.value;
return (title || '').trim();
}
get id(): string {
const { id } = this.form.value;
return (id || '').trim();
}
get description(): string {
const { description } = this.form.value;
return (description || '').trim();
}
get visibility(): string {
return this.visibilityOption || '';
}
submit() {
const { form, dialog } = this;
if (!form.valid) {
return;
}
this.create().subscribe(
(node: SiteEntry) => {
this.success.emit(node);
dialog.close(node);
},
(error) => this.handleError(error)
);
}
visibilityChangeHandler(event) {
this.visibilityOption = event.value;
}
private create(): Observable<SiteEntry> {
const { title, id, description, visibility } = this;
const siteBody = <SiteBodyCreate> {
id,
title,
description,
visibility
};
return from(this.alfrescoApiService.sitesApi.createSite(siteBody));
}
private sanitize(input: string) {
return input.replace(/[\s\s]+/g, '-').replace(/[^A-Za-z0-9-]/g, '');
}
private canGenerateId(title) {
return Boolean(title.replace(/[^A-Za-z0-9-]/g, '').length);
}
private handleError(error: any): any {
try {
const {
error: { statusCode }
} = JSON.parse(error.message);
if (statusCode === 409) {
this.form.controls['id'].setErrors({
message: 'LIBRARY.ERRORS.CONFLICT'
});
}
} catch (error) {
}
return error;
}
private async checkLibraryNameExists(libraryTitle: string) {
let entries = [];
try {
entries = (await this.findLibraryByTitle(libraryTitle)).list.entries;
} catch {
entries = [];
}
if (entries.length) {
this.libraryTitleExists = entries[0].entry.title.toLowerCase() === libraryTitle.toLowerCase();
} else {
this.libraryTitleExists = false;
}
}
private async findLibraryByTitle(libraryTitle: string): Promise<SitePaging> {
return this.alfrescoApiService
.getInstance()
.core.queriesApi.findSites(libraryTitle, {
maxItems: 1,
fields: ['title']
});
}
private forbidSpecialCharacters({ value }: FormControl) {
if (value === null || value.length === 0) {
return null;
}
const validCharacters: RegExp = /[^A-Za-z0-9-]/;
const isValid: boolean = !validCharacters.test(value);
return isValid
? null
: {
message: 'LIBRARY.ERRORS.ILLEGAL_CHARACTERS'
};
}
private forbidOnlySpaces({ value }: FormControl) {
if (value === null || value.length === 0) {
return null;
}
const isValid: boolean = !!(value || '').trim();
return isValid
? null
: {
message: 'LIBRARY.ERRORS.ONLY_SPACES'
};
}
private createSiteIdValidator() {
let timer;
return (control: AbstractControl) => {
if (timer) {
clearTimeout(timer);
}
return new Promise((resolve) => {
timer = setTimeout(() => {
return from(
this.alfrescoApiService.sitesApi.getSite(control.value)
).subscribe(
() => resolve({ message: 'LIBRARY.ERRORS.EXISTENT_SITE' }),
() => resolve(null)
);
}, 300);
});
};
}
}

View File

@@ -0,0 +1,47 @@
<h2 mat-dialog-title>
{{ 'CORE.FILE_DIALOG.FILE_LOCK' | translate }}
</h2>
<mat-dialog-content>
<br />
<form [formGroup]="form" (submit)="submit()">
<mat-checkbox data-automation-id="adf-lock-node-checkbox" class="adf-lock-file-name" [formControl]="form.controls['isLocked']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.FILE_LOCK_CHECKBOX' | translate }} <strong>"{{ nodeName }}"</strong>
</mat-checkbox>
<br />
<div *ngIf="form.value.isLocked">
<mat-checkbox class="adf-lock-file-name" [formControl]="form.controls['allowOwner']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.ALLOW_OTHERS_CHECKBOX' | translate }}
</mat-checkbox>
<br />
<mat-checkbox class="adf-lock-file-name" [formControl]="form.controls['isTimeLock']" ngDefaultControl>
{{ 'CORE.FILE_DIALOG.TIME_LOCK_CHECKBOX' | translate }}
</mat-checkbox>
<br />
<mat-form-field *ngIf="form.value.isTimeLock">
<mat-datetimepicker-toggle [for]="datetimePicker" matSuffix></mat-datetimepicker-toggle>
<mat-datetimepicker #datetimePicker type="datetime" openOnFocus="true" timeInterval="1"></mat-datetimepicker>
<input matInput [formControl]="form.controls['time']" [matDatetimepicker]="datetimePicker" required autocomplete="false">
</mat-form-field>
</div>
</form>
<br />
</mat-dialog-content>
<mat-dialog-actions class="adf-dialog-buttons">
<span class="adf-fill-remaining-space"></span>
<button mat-button mat-dialog-close data-automation-id="lock-dialog-btn-cancel">
{{ 'CORE.FILE_DIALOG.CANCEL_BUTTON.LABEL' | translate }}
</button>
<button class="adf-dialog-action-button" mat-button (click)="submit()">
{{ 'CORE.FILE_DIALOG.SAVE_BUTTON.LABEL' | translate }}
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,144 @@
/*!
* @license
* Copyright 2019 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 moment from 'moment-es6';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { ComponentFixture } from '@angular/core/testing';
import { MatDialogRef } from '@angular/material';
import { AlfrescoApiService, setupTestBed } from '@alfresco/adf-core';
import { NodeBodyLock } from '@alfresco/js-api';
import { NodeLockDialogComponent } from './node-lock.dialog';
import { ContentTestingModule } from '../testing/content.testing.module';
describe('NodeLockDialogComponent', () => {
let fixture: ComponentFixture<NodeLockDialogComponent>;
let component: NodeLockDialogComponent;
let alfrescoApi: AlfrescoApiService;
let expiryDate;
const dialogRef = {
close: jasmine.createSpy('close')
};
setupTestBed({
imports: [ContentTestingModule],
providers: [
{ provide: MatDialogRef, useValue: dialogRef }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(NodeLockDialogComponent);
component = fixture.componentInstance;
alfrescoApi = TestBed.get(AlfrescoApiService);
});
afterEach(() => {
fixture.destroy();
});
describe('Node lock dialog component', () => {
beforeEach(() => {
jasmine.clock().mockDate(new Date());
expiryDate = moment().add(1, 'minutes');
component.data = {
node: {
id: 'node-id',
name: 'node-name',
isLocked: true,
properties: {
['cm:lockType']: 'WRITE_LOCK',
['cm:expiryDate']: expiryDate
}
},
onError: () => {
}
};
fixture.detectChanges();
});
it('should init dialog with form inputs', () => {
expect(component.nodeName).toBe('node-name');
expect(component.form.value.isLocked).toBe(true);
expect(component.form.value.allowOwner).toBe(true);
expect(component.form.value.isTimeLock).toBe(true);
expect(component.form.value.time.format()).toBe(expiryDate.format());
});
it('should update form inputs', () => {
const newTime = moment();
component.form.controls['isLocked'].setValue(false);
component.form.controls['allowOwner'].setValue(false);
component.form.controls['isTimeLock'].setValue(false);
component.form.controls['time'].setValue(newTime);
expect(component.form.value.isLocked).toBe(false);
expect(component.form.value.allowOwner).toBe(false);
expect(component.form.value.isTimeLock).toBe(false);
expect(component.form.value.time.format()).toBe(newTime.format());
});
it('should submit the form and lock the node', () => {
spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.resolve({}));
component.submit();
expect(alfrescoApi.nodesApi.lockNode).toHaveBeenCalledWith(
'node-id',
new NodeBodyLock({
'timeToExpire': 60,
'type': 'ALLOW_OWNER_CHANGES',
'lifetime': 'PERSISTENT'
})
);
});
it('should submit the form and unlock the node', () => {
spyOn(alfrescoApi.nodesApi, 'unlockNode').and.returnValue(Promise.resolve({}));
component.form.controls['isLocked'].setValue(false);
component.submit();
expect(alfrescoApi.nodesApi.unlockNode).toHaveBeenCalledWith('node-id');
});
it('should call dialog to close with form data when submit is successfully', fakeAsync(() => {
const node = { entry: {} };
spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.resolve(node));
component.submit();
tick();
fixture.detectChanges();
expect(dialogRef.close).toHaveBeenCalledWith(node.entry);
}));
it('should call onError if submit fails', fakeAsync(() => {
spyOn(alfrescoApi.nodesApi, 'lockNode').and.returnValue(Promise.reject('error'));
spyOn(component.data, 'onError');
component.submit();
tick();
fixture.detectChanges();
expect(component.data.onError).toHaveBeenCalled();
}));
});
});

View File

@@ -0,0 +1,95 @@
/*!
* @license
* Copyright 2019 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 moment from 'moment-es6';
import { Component, Inject, OnInit, Optional } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { FormBuilder, FormGroup } from '@angular/forms';
import { NodeBodyLock, Node, NodeEntry } from '@alfresco/js-api';
import { AlfrescoApiService } from '@alfresco/adf-core';
@Component({
selector: 'adf-node-lock',
styleUrls: ['./folder.dialog.scss'],
templateUrl: './node-lock.dialog.html'
})
export class NodeLockDialogComponent implements OnInit {
form: FormGroup;
node: Node = null;
nodeName: string;
constructor(
private formBuilder: FormBuilder,
public dialog: MatDialogRef<NodeLockDialogComponent>,
private alfrescoApi: AlfrescoApiService,
@Optional()
@Inject(MAT_DIALOG_DATA)
public data: any
) {
}
ngOnInit() {
const { node } = this.data;
this.nodeName = node.name;
this.form = this.formBuilder.group({
isLocked: node.isLocked || false,
allowOwner: node.properties['cm:lockType'] === 'WRITE_LOCK',
isTimeLock: !!node.properties['cm:expiryDate'],
time: !!node.properties['cm:expiryDate'] ? moment(node.properties['cm:expiryDate']) : moment()
});
}
private get lockTimeInSeconds(): number {
if (this.form.value.isTimeLock) {
const duration = moment.duration(moment(this.form.value.time).diff(moment()));
return duration.asSeconds();
}
return 0;
}
private get nodeBodyLock(): NodeBodyLock {
return new NodeBodyLock({
'timeToExpire': this.lockTimeInSeconds,
'type': this.form.value.allowOwner ? 'ALLOW_OWNER_CHANGES' : 'FULL',
'lifetime': 'PERSISTENT'
});
}
private toggleLock(): Promise<NodeEntry> {
const { alfrescoApi: { nodesApi }, data: { node } } = this;
if (this.form.value.isLocked) {
return nodesApi.lockNode(node.id, this.nodeBodyLock);
}
return nodesApi.unlockNode(node.id);
}
submit(): void {
this.toggleLock()
.then((node: NodeEntry) => {
this.data.node.isLocked = this.form.value.isLocked;
this.dialog.close(node.entry);
})
.catch((error: any) => this.data.onError(error));
}
}

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 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 './folder.dialog';
export * from './node-lock.dialog';
export * from './confirm.dialog';
export * from './dialog.module';
export * from './library/library.dialog';
export * from './folder-name.validators';

Some files were not shown because too many files have changed in this diff Show More