diff --git a/angular.json b/angular.json
index 27ea6c2..8d29d2a 100644
--- a/angular.json
+++ b/angular.json
@@ -1393,6 +1393,49 @@
}
}
},
+ "ui-group": {
+ "projectType": "library",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss"
+ }
+ },
+ "root": "projects/ui-group",
+ "sourceRoot": "projects/ui-group/src",
+ "prefix": "ucap-group",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-ng-packagr:build",
+ "options": {
+ "tsConfig": "projects/ui-group/tsconfig.lib.json",
+ "project": "projects/ui-group/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/ui-group/tsconfig.lib.prod.json"
+ }
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "main": "projects/ui-group/src/test.ts",
+ "tsConfig": "projects/ui-group/tsconfig.spec.json",
+ "karmaConfig": "projects/ui-group/karma.conf.js"
+ }
+ },
+ "lint": {
+ "builder": "@angular-devkit/build-angular:tslint",
+ "options": {
+ "tsConfig": [
+ "projects/ui-group/tsconfig.lib.json",
+ "projects/ui-group/tsconfig.spec.json"
+ ],
+ "exclude": ["**/node_modules/**"]
+ }
+ }
+ }
+ },
"ui-skin-default": {
"projectType": "library",
"schematics": {
diff --git a/package-lock.json b/package-lock.json
index ee92ed6..58107df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3383,13 +3383,18 @@
},
"@ucap/ng-ui": {
"version": "file:pack/ucap-ng-ui-0.0.4.tgz",
- "integrity": "sha512-DcrZx55uGvvc70vUxP2fkgQhORMMBgkKWRdG5zH1PpgB4wY1Od887DgFJISEQzbcFNBNr6TiX+I8e64maGQp0A=="
+ "integrity": "sha512-DcrZx55uGvvc70vUxP2fkgQhORMMBgkKWRdG5zH1PpgB4wY1Od887DgFJISEQzbcFNBNr6TiX+I8e64maGQp0A==",
+ "dev": true
},
"@ucap/ng-ui-authentication": {
"version": "file:pack/ucap-ng-ui-authentication-0.0.16.tgz",
"integrity": "sha512-j9JLn3btK2yVF3cthELaSwn5oe6qYW7z+knT8O9b/a36xuHGeMZLr60IGM6cF1QgRdFpFWaK2oN0DgdnU/s8Iw==",
"dev": true
},
+ "@ucap/ng-ui-group": {
+ "version": "file:pack/ucap-ng-ui-group-0.0.3.tgz",
+ "integrity": "sha512-VH0X1xy2IaHwe0ihkKn9N3fmpz8KD5xAmf8tSTC/DbvoxCI2r7nlRddvh9emthOkdKIqXhbaO20Q1oKrXUGvxg=="
+ },
"@ucap/ng-ui-organization": {
"version": "file:pack/ucap-ng-ui-organization-0.0.2.tgz",
"integrity": "sha512-IzTDv19feOL76nxRrRJ3PMKuV86HQJrqUXHMurkUjvlQnZM/1yMa7gNSvQcERprwnMCAwB42VTvbI+mN5AxoAA==",
@@ -6603,7 +6608,7 @@
"version": "4.0.0",
"resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/crypto-js/-/crypto-js-4.0.0.tgz",
"integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==",
- "optional": true
+ "dev": true
},
"crypto-random-string": {
"version": "2.0.0",
@@ -12052,6 +12057,16 @@
}
}
},
+ "ngx-perfect-scrollbar": {
+ "version": "9.0.0",
+ "resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/ngx-perfect-scrollbar/-/ngx-perfect-scrollbar-9.0.0.tgz",
+ "integrity": "sha512-jiFrOLONf/w2PjSKkEjQeTnMdlMVcQgjzIrYcsor1HWTmE+95J2sZAd/WF4zoutbpIqfU8VQQoAp8HOa7U1c/g==",
+ "dev": true,
+ "requires": {
+ "perfect-scrollbar": "1.5.0",
+ "resize-observer-polyfill": "^1.5.0"
+ }
+ },
"nice-try": {
"version": "1.0.5",
"resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/nice-try/-/nice-try-1.0.5.tgz",
@@ -13178,6 +13193,12 @@
"integrity": "sha512-KGuODSTV6hcgdZvDrIDBUkN0utcAVj1LL7FfGbM0viKTtCHmtZcuEJ+lGqsp0fTFkGqesdtemV2yUSMeyy3ddA==",
"dev": true
},
+ "perfect-scrollbar": {
+ "version": "1.5.0",
+ "resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/perfect-scrollbar/-/perfect-scrollbar-1.5.0.tgz",
+ "integrity": "sha512-NrNHJn5mUGupSiheBTy6x+6SXCFbLlm8fVZh9moIzw/LgqElN5q4ncR4pbCBCYuCJ8Kcl9mYM0NgDxvW+b4LxA==",
+ "dev": true
+ },
"performance-now": {
"version": "2.1.0",
"resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/performance-now/-/performance-now-2.1.0.tgz",
@@ -14461,7 +14482,7 @@
"version": "0.3.4",
"resolved": "http://10.81.13.221:8081/nexus/repository/npm-all/queueing-subject/-/queueing-subject-0.3.4.tgz",
"integrity": "sha512-sdpymi9eq80oZyg74NrIGr1GHKIDRmBLZp+xqOct8Do5KpKalPsSz9NxApZb0S2j+EEDMzDlosBN5NJGFLmS7A==",
- "optional": true
+ "dev": true
},
"quick-lru": {
"version": "1.1.0",
diff --git a/package.json b/package.json
index fea8066..600c1b1 100644
--- a/package.json
+++ b/package.json
@@ -45,10 +45,11 @@
"build:store-chat": "node ./scripts/build.js store-chat",
"build:store-group": "node ./scripts/build.js store-group",
"build:store-organization": "node ./scripts/build.js store-organization",
- "build:ui:all": "npm-run-all -s build:ui build:ui-organization build:ui-authentication",
+ "build:ui:all": "npm-run-all -s build:ui build:ui-organization build:ui-authentication build:ui-group",
"build:ui": "node ./scripts/build.js ui",
"build:ui-organization": "node ./scripts/build.js ui-organization",
"build:ui-authentication": "node ./scripts/build.js ui-authentication",
+ "build:ui-group": "node ./scripts/build.js ui-group",
"build:ui-skin:all": "npm-run-all -s build:ui-skin-default",
"build:ui-skin-default": "node ./scripts/build.js ui-skin-default useScssBundle",
"publish:all": "npm-run-all -s publish:logger publish:core publish:util:all publish:api:all publish:protocol:all publish:native:all publish:store:all publish:ui:all publish:ui-skin:all",
@@ -90,10 +91,11 @@
"publish:store-chat": "cd ./dist/store-chat && npm publish",
"publish:store-group": "cd ./dist/store-group && npm publish",
"publish:store-organization": "cd ./dist/store-organization && npm publish",
- "publish:ui:all": "npm-run-all -s publish:ui publish:ui-organization publish:ui-authentication",
+ "publish:ui:all": "npm-run-all -s publish:ui publish:ui-organization publish:ui-authentication publish:ui-group",
"publish:ui": "cd ./dist/ui && npm publish",
"publish:ui-organization": "cd ./dist/ui-organization && npm publish",
"publish:ui-authentication": "cd ./dist/ui-authentication && npm publish",
+ "publish:ui-group": "cd ./dist/ui-group && npm publish",
"publish:ui-skin:all": "npm-run-all -s publish:ui-skin-default",
"publish:ui-skin-default": "cd ./dist/ui-skin-default && npm publish",
"test": "ng test",
@@ -181,6 +183,7 @@
"@ucap/ng-store-organization": "file:pack/ucap-ng-store-organization-0.0.4.tgz",
"@ucap/ng-ui": "file:pack/ucap-ng-ui-0.0.4.tgz",
"@ucap/ng-ui-authentication": "file:pack/ucap-ng-ui-authentication-0.0.16.tgz",
+ "@ucap/ng-ui-group": "file:pack/ucap-ng-ui-group-0.0.3.tgz",
"@ucap/ng-ui-organization": "file:pack/ucap-ng-ui-organization-0.0.2.tgz",
"@ucap/ng-ui-skin-default": "file:pack/ucap-ng-ui-skin-default-0.0.1.tgz",
"@ucap/ng-web-socket": "file:pack/ucap-ng-web-socket-0.0.2.tgz",
@@ -209,6 +212,7 @@
"babel-loader": "^8.1.0",
"codelyzer": "^5.2.1",
"concurrently": "^5.1.0",
+ "crypto-js": "^4.0.0",
"detect-browser": "^5.0.0",
"file-type": "^14.1.4",
"fs-extra": "^9.0.0",
@@ -225,8 +229,10 @@
"moment-timezone": "^0.5.28",
"move-cli": "^1.2.1",
"ng-packagr": "^9.0.3",
+ "ngx-perfect-scrollbar": "^9.0.0",
"npm-run-all": "^4.1.5",
"protractor": "~5.4.3",
+ "queueing-subject": "^0.3.4",
"rimraf": "^3.0.2",
"rxjs": "~6.5.4",
"scss-bundle": "^3.1.1",
@@ -236,9 +242,5 @@
"typescript": "~3.7.5",
"webpack-bundle-analyzer": "^3.6.1",
"zone.js": "~0.10.2"
- },
- "optionalDependencies": {
- "queueing-subject": "^0.3.4",
- "crypto-js": "^4.0.0"
}
}
diff --git a/projects/ui-group/README.md b/projects/ui-group/README.md
new file mode 100644
index 0000000..f331315
--- /dev/null
+++ b/projects/ui-group/README.md
@@ -0,0 +1,25 @@
+# UiGroup
+
+This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.2.
+
+## Code scaffolding
+
+Run `ng generate component component-name --project ui-group` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ui-group`.
+
+> Note: Don't forget to add `--project ui-group` or else it will be added to the default project in your `angular.json` file.
+
+## Build
+
+Run `ng build ui-group` to build the project. The build artifacts will be stored in the `dist/` directory.
+
+## Publishing
+
+After building your library with `ng build ui-group`, go to the dist folder `cd dist/ui-group` and run `npm publish`.
+
+## Running unit tests
+
+Run `ng test ui-group` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+## Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
diff --git a/projects/ui-group/karma.conf.js b/projects/ui-group/karma.conf.js
new file mode 100644
index 0000000..b33ab95
--- /dev/null
+++ b/projects/ui-group/karma.conf.js
@@ -0,0 +1,32 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('karma-coverage-istanbul-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma')
+ ],
+ client: {
+ clearContext: false // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageIstanbulReporter: {
+ dir: require('path').join(__dirname, '../../coverage/ui-group'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true
+ },
+ reporters: ['progress', 'kjhtml'],
+ port: 9876,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['Chrome'],
+ singleRun: false,
+ restartOnFileChange: true
+ });
+};
diff --git a/projects/ui-group/ng-package.json b/projects/ui-group/ng-package.json
new file mode 100644
index 0000000..dcf91b0
--- /dev/null
+++ b/projects/ui-group/ng-package.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../dist/ui-group",
+ "lib": {
+ "entryFile": "src/public-api.ts",
+ "umdModuleIds": {
+ "@ucap/core": "@ucap/core",
+ "@ucap/ng-ui": "@ucap/ng-ui"
+ }
+ }
+}
diff --git a/projects/ui-group/package.json b/projects/ui-group/package.json
new file mode 100644
index 0000000..fddbba6
--- /dev/null
+++ b/projects/ui-group/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@ucap/ng-ui-group",
+ "version": "0.0.3",
+ "publishConfig": {
+ "registry": "http://10.81.13.221:8081/nexus/repository/npm-ucap/"
+ },
+ "peerDependencies": {
+ "@angular/cdk": "^9.0.0",
+ "@angular/common": "^9.0.2",
+ "@angular/core": "^9.0.2",
+ "@angular/material": "^9.0.0",
+ "@ucap/core": "~0.0.1",
+ "@ucap/ng-ui": "~0.0.1",
+ "tslib": "^1.10.0"
+ }
+}
diff --git a/projects/ui-group/src/lib/components/expansion-list.component.html b/projects/ui-group/src/lib/components/expansion-list.component.html
new file mode 100644
index 0000000..4eb8b41
--- /dev/null
+++ b/projects/ui-group/src/lib/components/expansion-list.component.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/ui-group/src/lib/components/expansion-list.component.scss b/projects/ui-group/src/lib/components/expansion-list.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/projects/ui-group/src/lib/components/expansion-list.component.spec.ts b/projects/ui-group/src/lib/components/expansion-list.component.spec.ts
new file mode 100644
index 0000000..6ea8bfe
--- /dev/null
+++ b/projects/ui-group/src/lib/components/expansion-list.component.spec.ts
@@ -0,0 +1,27 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { ExpansionListComponent } from './expansion-list.component';
+
+describe('ucap::ui-group::ExpansionListComponent', () => {
+ let component: ExpansionListComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ExpansionListComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ExpansionListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/projects/ui-group/src/lib/components/expansion-list.component.stories.ts b/projects/ui-group/src/lib/components/expansion-list.component.stories.ts
new file mode 100644
index 0000000..7a717ae
--- /dev/null
+++ b/projects/ui-group/src/lib/components/expansion-list.component.stories.ts
@@ -0,0 +1,16 @@
+import { action } from '@storybook/addon-actions';
+import { linkTo } from '@storybook/addon-links';
+
+import { ExpansionListComponent } from './expansion-list.component';
+
+export default {
+ title: 'ExpansionListComponent',
+ component: ExpansionListComponent
+};
+
+export const Text = () => ({
+ component: ExpansionListComponent,
+ props: {
+ text: 'Hello ExpansionListComponent'
+ }
+});
diff --git a/projects/ui-group/src/lib/components/expansion-list.component.ts b/projects/ui-group/src/lib/components/expansion-list.component.ts
new file mode 100644
index 0000000..723d057
--- /dev/null
+++ b/projects/ui-group/src/lib/components/expansion-list.component.ts
@@ -0,0 +1,286 @@
+import {
+ Component,
+ OnInit,
+ OnDestroy,
+ Input,
+ Output,
+ EventEmitter,
+ ViewChild,
+ ContentChild,
+ TemplateRef
+} from '@angular/core';
+
+import {
+ VirtualScrollStrategy,
+ CdkVirtualScrollViewport
+} from '@angular/cdk/scrolling';
+import { FlatTreeControl } from '@angular/cdk/tree';
+
+import { MatTreeFlattener, MatTree } from '@angular/material/tree';
+
+import { UserInfo, GroupDetailData } from '@ucap/protocol-sync';
+
+import { VirtualScrollTreeFlatDataSource } from '@ucap/ng-ui';
+import { UserInfoSS, UserInfoF, UserInfoDN } from '@ucap/protocol-query';
+
+export enum NodeType {
+ None = 'None',
+ Profile = 'Profile',
+ Favorite = 'Favorite',
+ Buddy = 'Buddy',
+ Default = 'Default'
+}
+
+export interface GroupNode {
+ nodeType: NodeType;
+ userInfo?: UserInfo;
+ groupDetail?: GroupDetailData;
+ children?: GroupNode[];
+}
+
+export interface FlatNode {
+ expandable: boolean;
+ level: number;
+ node: GroupNode;
+}
+
+@Component({
+ selector: 'ucap-group-expansion-list',
+ templateUrl: './expansion-list.component.html',
+ styleUrls: ['./expansion-list.component.scss']
+})
+export class ExpansionListComponent implements OnInit, OnDestroy {
+ @Input()
+ virtualScrollStrategy: VirtualScrollStrategy;
+
+ @Input()
+ set profile(userInfo: UserInfo) {
+ if (!userInfo) {
+ this.profileNodes = [];
+ } else {
+ const node: GroupNode = {
+ nodeType: NodeType.Profile,
+ userInfo,
+ children: []
+ };
+
+ this.profileNodes = [node];
+ }
+
+ this.refreshNodes();
+ }
+
+ @Input()
+ set favorites(userInfos: UserInfo[]) {
+ if (!userInfos || 0 === userInfos.length) {
+ this.favoriteNodes = [];
+ } else {
+ const node: GroupNode = {
+ nodeType: NodeType.Favorite,
+ groupDetail: {
+ seq: -9999,
+ name: NodeType.Favorite,
+ isActive: true,
+ userSeqs: userInfos.map((userInfo) => String(userInfo.seq))
+ } as GroupDetailData,
+ children: []
+ };
+
+ userInfos.forEach((userInfo) => {
+ node.children.push({
+ nodeType: NodeType.Favorite,
+ userInfo
+ });
+ });
+
+ this.favoriteNodes = [node];
+ }
+
+ this.refreshNodes();
+ }
+
+ @Input()
+ set groupBuddies(list: { group: GroupDetailData; buddyList: UserInfo[] }[]) {
+ if (!list || 0 === list.length) {
+ this.buddyNodes = [];
+ } else {
+ for (const item of list) {
+ let nodeType = NodeType.Buddy;
+ if (0 === item.group.seq) {
+ nodeType = NodeType.Default;
+ }
+
+ const node: GroupNode = {
+ nodeType,
+ groupDetail: item.group,
+ children: []
+ };
+
+ item.buddyList.sort((a, b) =>
+ a.order < b.order
+ ? -1
+ : a.order > b.order
+ ? 1
+ : a.name < b.name
+ ? -1
+ : a.name > b.name
+ ? 1
+ : 0
+ );
+
+ item.buddyList.forEach((userInfo) => {
+ node.children.push({
+ nodeType,
+ groupDetail: item.group,
+ userInfo
+ });
+ });
+
+ this.buddyNodes.push(node);
+ }
+ }
+
+ this.refreshNodes();
+ }
+
+ @Input()
+ checkable = false;
+
+ @Input()
+ selectedUserList?: (UserInfo | UserInfoSS | UserInfoF | UserInfoDN)[] = [];
+
+ @Input()
+ unselectableUserList?: (
+ | UserInfo
+ | UserInfoSS
+ | UserInfoF
+ | UserInfoDN
+ )[] = [];
+
+ @ViewChild('treeList', { static: false })
+ treeList: MatTree;
+
+ @ViewChild('cvsvList', { static: false })
+ cvsvList: CdkVirtualScrollViewport;
+
+ @ContentChild('[ucapGroupExpansionList="node"]', { read: TemplateRef })
+ nodeTemplate: TemplateRef;
+
+ @ContentChild('[ucapGroupExpansionList="favoriteHeader"]', {
+ read: TemplateRef
+ })
+ favoriteHeaderTemplate: TemplateRef;
+
+ @ContentChild('[ucapGroupExpansionList="defaultHeader"]', {
+ read: TemplateRef
+ })
+ defaultHeaderTemplate: TemplateRef;
+
+ @ContentChild('[ucapGroupExpansionList="buddyHeader"]', { read: TemplateRef })
+ buddyHeaderTemplate: TemplateRef;
+
+ rootNodeList: GroupNode[] = [];
+ treeControl: FlatTreeControl;
+ treeFlattener: MatTreeFlattener;
+ dataSource: VirtualScrollTreeFlatDataSource;
+
+ NodeType = NodeType;
+
+ private profileNodes: GroupNode[] = [];
+ private favoriteNodes: GroupNode[] = [];
+ private buddyNodes: GroupNode[] = [];
+
+ constructor() {
+ this.treeControl = new FlatTreeControl(
+ (node) => node.level,
+ (node) => node.expandable
+ );
+
+ this.treeFlattener = new MatTreeFlattener(
+ (node: GroupNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ level,
+ nodeType: node.nodeType,
+ node
+ };
+ },
+ (node) => node.level,
+ (node) => node.expandable,
+ (node) => node.children
+ );
+
+ this.dataSource = new VirtualScrollTreeFlatDataSource(
+ this.treeControl,
+ this.treeFlattener
+ );
+ }
+
+ ngOnInit(): void {}
+
+ ngOnDestroy(): void {}
+
+ onClickHeaderMenu(event: MouseEvent, node: FlatNode) {}
+
+ isCheckedGroup(node: FlatNode): boolean {
+ const groupDetail = node.node.groupDetail;
+
+ if (!groupDetail || groupDetail === undefined) {
+ return false;
+ }
+
+ if (groupDetail.userSeqs.length === 0) {
+ return false;
+ }
+
+ if (!!this.selectedUserList && this.selectedUserList.length > 0) {
+ let allExist = true;
+ groupDetail.userSeqs.some((seq) => {
+ if (
+ this.selectedUserList.filter((item) => item.seq === seq).length === 0
+ ) {
+ allExist = false;
+ return true;
+ }
+ });
+ return allExist;
+ }
+ return false;
+ }
+
+ isCheckableGroup(node: FlatNode): boolean {
+ if (!!this.unselectableUserList && this.unselectableUserList.length > 0) {
+ const groupDetail = node.node.groupDetail;
+ let allExist = true;
+ groupDetail.userSeqs.some((seq) => {
+ if (
+ this.unselectableUserList.filter((item) => item.seq === seq)
+ .length === 0
+ ) {
+ allExist = false;
+ return true;
+ }
+ });
+
+ if (allExist) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ onChangeCheckGroup(value: boolean, node: FlatNode) {}
+
+ isHeader = (_: number, node: FlatNode) =>
+ NodeType.Profile !== node.node.nodeType && 0 === node.level;
+
+ private refreshNodes() {
+ this.rootNodeList = [
+ ...this.profileNodes,
+ ...this.favoriteNodes,
+ ...this.buddyNodes
+ ];
+ this.dataSource.data = this.rootNodeList;
+ }
+}
diff --git a/projects/ui-group/src/lib/config/module-config.ts b/projects/ui-group/src/lib/config/module-config.ts
new file mode 100644
index 0000000..d158cc6
--- /dev/null
+++ b/projects/ui-group/src/lib/config/module-config.ts
@@ -0,0 +1,4 @@
+import { ModuleConfig as CoreModuleConfig } from '@ucap/core';
+
+// tslint:disable-next-line: no-empty-interface
+export interface ModuleConfig extends CoreModuleConfig {}
diff --git a/projects/ui-group/src/lib/config/token.ts b/projects/ui-group/src/lib/config/token.ts
new file mode 100644
index 0000000..2f4306f
--- /dev/null
+++ b/projects/ui-group/src/lib/config/token.ts
@@ -0,0 +1,5 @@
+import { InjectionToken } from '@angular/core';
+
+export const _MODULE_CONFIG = new InjectionToken(
+ '@ucap/ui-group config of module'
+);
diff --git a/projects/ui-group/src/lib/group-ui.module.ts b/projects/ui-group/src/lib/group-ui.module.ts
new file mode 100644
index 0000000..3c6a14e
--- /dev/null
+++ b/projects/ui-group/src/lib/group-ui.module.ts
@@ -0,0 +1,61 @@
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ScrollingModule } from '@angular/cdk/scrolling';
+
+import { MatRippleModule } from '@angular/material/core';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+import { MatTreeModule } from '@angular/material/tree';
+
+import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
+
+import { UiModule } from '@ucap/ng-ui';
+
+import { ModuleConfig } from './config/module-config';
+import { _MODULE_CONFIG } from './config/token';
+
+import { ExpansionListComponent } from './components/expansion-list.component';
+
+const COMPONENTS = [ExpansionListComponent];
+const DIALOGS = [];
+const PIPES = [];
+const DIRECTIVES = [];
+const SERVICES = [];
+
+@NgModule({
+ declarations: [],
+ imports: [],
+ exports: []
+})
+export class GroupUiRootModule {}
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ScrollingModule,
+
+ MatButtonModule,
+ MatCheckboxModule,
+ MatIconModule,
+ MatRippleModule,
+ MatTreeModule,
+
+ PerfectScrollbarModule,
+ UiModule
+ ],
+ exports: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
+ declarations: [...COMPONENTS, ...DIRECTIVES, ...PIPES],
+ entryComponents: [...DIALOGS]
+})
+export class GroupUiModule {
+ public static forRoot(
+ config: ModuleConfig
+ ): ModuleWithProviders {
+ return {
+ ngModule: GroupUiRootModule,
+ providers: [{ provide: _MODULE_CONFIG, useValue: config }, ...SERVICES]
+ };
+ }
+}
diff --git a/projects/ui-group/src/public-api.ts b/projects/ui-group/src/public-api.ts
new file mode 100644
index 0000000..2001585
--- /dev/null
+++ b/projects/ui-group/src/public-api.ts
@@ -0,0 +1,9 @@
+/*
+ * Public API Surface of ui-group
+ */
+
+export * from './lib/config/module-config';
+
+export * from './lib/components/expansion-list.component';
+
+export * from './lib/group-ui.module';
diff --git a/projects/ui-group/src/test.ts b/projects/ui-group/src/test.ts
new file mode 100644
index 0000000..303b32a
--- /dev/null
+++ b/projects/ui-group/src/test.ts
@@ -0,0 +1,26 @@
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/dist/zone';
+import 'zone.js/dist/zone-testing';
+import { getTestBed } from '@angular/core/testing';
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting
+} from '@angular/platform-browser-dynamic/testing';
+
+declare const require: {
+ context(path: string, deep?: boolean, filter?: RegExp): {
+ keys(): string[];
+ (id: string): T;
+ };
+};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting()
+);
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().map(context);
diff --git a/projects/ui-group/tsconfig.lib.json b/projects/ui-group/tsconfig.lib.json
new file mode 100644
index 0000000..4b5d4af
--- /dev/null
+++ b/projects/ui-group/tsconfig.lib.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/lib",
+ "target": "es2015",
+ "declaration": true,
+ "inlineSources": true,
+ "types": [],
+ "lib": [
+ "dom",
+ "es2018"
+ ]
+ },
+ "angularCompilerOptions": {
+ "skipTemplateCodegen": true,
+ "strictMetadataEmit": true,
+ "enableResourceInlining": true
+ },
+ "exclude": [
+ "src/test.ts",
+ "**/*.spec.ts"
+ ]
+}
diff --git a/projects/ui-group/tsconfig.lib.prod.json b/projects/ui-group/tsconfig.lib.prod.json
new file mode 100644
index 0000000..cbae794
--- /dev/null
+++ b/projects/ui-group/tsconfig.lib.prod.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "angularCompilerOptions": {
+ "enableIvy": false
+ }
+}
diff --git a/projects/ui-group/tsconfig.spec.json b/projects/ui-group/tsconfig.spec.json
new file mode 100644
index 0000000..16da33d
--- /dev/null
+++ b/projects/ui-group/tsconfig.spec.json
@@ -0,0 +1,17 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/spec",
+ "types": [
+ "jasmine",
+ "node"
+ ]
+ },
+ "files": [
+ "src/test.ts"
+ ],
+ "include": [
+ "**/*.spec.ts",
+ "**/*.d.ts"
+ ]
+}
diff --git a/projects/ui-group/tslint.json b/projects/ui-group/tslint.json
new file mode 100644
index 0000000..441e77a
--- /dev/null
+++ b/projects/ui-group/tslint.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tslint.json",
+ "rules": {
+ "directive-selector": [true, "attribute", "ucapGroup", "camelCase"],
+ "component-selector": [true, "element", "ucap-group", "kebab-case"]
+ }
+}