├── tests ├── _data │ └── .gitkeep ├── _output │ └── .gitignore ├── acceptance │ ├── _bootstrap.php │ ├── AboutCest.php │ ├── HomeCest.php │ ├── LoginCest.php │ └── ContactCest.php ├── functional │ ├── _bootstrap.php │ └── LoginFormCest.php ├── unit │ ├── _bootstrap.php │ └── models │ │ ├── UserTest.php │ │ ├── LoginFormTest.php │ │ └── ContactFormTest.php ├── _bootstrap.php ├── unit.suite.yml ├── acceptance.suite.yml.example ├── functional.suite.yml ├── bin │ ├── yii.bat │ └── yii └── _support │ ├── FunctionalTester.php │ ├── UnitTester.php │ └── AcceptanceTester.php ├── vue ├── src │ ├── core │ │ ├── components │ │ │ ├── index.js │ │ │ ├── org-chart │ │ │ │ └── src │ │ │ │ │ ├── lib │ │ │ │ │ └── lodash.js │ │ │ │ │ └── index.js │ │ │ ├── pages │ │ │ │ └── NotFoundComponent.vue │ │ │ ├── PageFooter.vue │ │ │ ├── NoDataAvailable.vue │ │ │ ├── sidebar │ │ │ │ ├── MenuService.js │ │ │ │ ├── SidebarItemContent.vue │ │ │ │ └── MenuItem.js │ │ │ ├── NoData.vue │ │ │ ├── PhotoVideoInput.vue │ │ │ ├── DropdownButtons.vue │ │ │ ├── ContentSpinner.vue │ │ │ ├── LikeUnlikeButton.vue │ │ │ ├── PageHeader.vue │ │ │ ├── sided-nav-layout │ │ │ │ └── SidedNavLayout.vue │ │ │ ├── navbar │ │ │ │ └── components │ │ │ │ │ └── SidebarToggle.vue │ │ │ └── SubmitButton.vue │ │ ├── scss │ │ │ ├── bootstrap │ │ │ │ ├── index.scss │ │ │ │ └── override │ │ │ │ │ └── nav.scss │ │ │ ├── mixins │ │ │ │ ├── callouts.scss │ │ │ │ ├── tooltips.scss │ │ │ │ ├── size.scss │ │ │ │ ├── outline.scss │ │ │ │ ├── forms.scss │ │ │ │ ├── panels.scss │ │ │ │ ├── dropdowns.scss │ │ │ │ ├── vendor-prefixes.scss │ │ │ │ ├── tables.scss │ │ │ │ ├── popovers.scss │ │ │ │ ├── navbar.scss │ │ │ │ ├── buttons.scss │ │ │ │ └── switches.scss │ │ │ ├── index.scss │ │ │ ├── main.scss │ │ │ └── mixins.scss │ │ ├── filters │ │ │ ├── index.js │ │ │ ├── dateFilter.js │ │ │ └── bytesFilter.js │ │ ├── index.js │ │ └── services │ │ │ ├── event-bus.js │ │ │ ├── dateService.js │ │ │ ├── userService.js │ │ │ └── fileService.js │ ├── plugins │ │ ├── font-awesome │ │ │ └── index.js │ │ ├── ckeditor.js │ │ ├── bootstrap-vue │ │ │ └── index.js │ │ ├── vue-clipboard2 │ │ │ └── index.js │ │ ├── index.js │ │ ├── vee-validate │ │ │ ├── custom-rules.js │ │ │ └── index.js │ │ ├── notification.js │ │ ├── confirm-dialog.js │ │ ├── alert-dialog.js │ │ ├── axios │ │ │ └── index.js │ │ ├── toaster.js │ │ └── bootstrap │ │ │ └── bootstrap.scss │ ├── store │ │ ├── modules │ │ │ ├── user │ │ │ │ ├── types.d.ts │ │ │ │ ├── getters.js │ │ │ │ ├── mutation-types.js │ │ │ │ ├── state.js │ │ │ │ ├── index.js │ │ │ │ ├── mutations.js │ │ │ │ └── actions.js │ │ │ ├── employee │ │ │ │ ├── index.js │ │ │ │ ├── state.js │ │ │ │ ├── mutation-types.js │ │ │ │ └── mutations.js │ │ │ ├── setup │ │ │ │ ├── index.js │ │ │ │ ├── state.js │ │ │ │ └── mutation-types.js │ │ │ └── workspaces │ │ │ │ ├── index.js │ │ │ │ └── state.js │ │ └── setup │ │ │ └── getters.js │ ├── index.scss │ ├── modules │ │ ├── index.js │ │ ├── Workspace │ │ │ ├── workspaceModule.js │ │ │ ├── components │ │ │ │ └── comment │ │ │ │ │ ├── AddCommentModel.js │ │ │ │ │ ├── DeleteComment.vue │ │ │ │ │ ├── ChildCommentItem.vue │ │ │ │ │ ├── AddComment.vue │ │ │ │ │ └── CommentItem.vue │ │ │ ├── invite │ │ │ │ └── WorkspaceInviteModel.js │ │ │ ├── view │ │ │ │ ├── files │ │ │ │ │ └── FolderFormModel.js │ │ │ │ ├── articles │ │ │ │ │ ├── ArticleFormModel.js │ │ │ │ │ └── ArticleView.vue │ │ │ │ ├── timeline │ │ │ │ │ └── TimelineFormModel.js │ │ │ │ └── about │ │ │ │ │ └── WorkspaceAbout.vue │ │ │ └── WorkspaceFormModel.js │ │ ├── setup │ │ │ ├── Setup.vue │ │ │ ├── countries │ │ │ │ ├── CountryModel.js │ │ │ │ ├── departments │ │ │ │ │ ├── DepartmentModel.js │ │ │ │ │ └── DepartmentListGroup.vue │ │ │ │ └── CountryModal.vue │ │ │ ├── invitations │ │ │ │ └── UserInvitationFormModel.js │ │ │ ├── employees │ │ │ │ ├── RoleModel.js │ │ │ │ ├── employeesService.js │ │ │ │ ├── UserDepartmentModel.js │ │ │ │ ├── EmployeeModel.js │ │ │ │ └── EmployeeList.vue │ │ │ └── setupModule.js │ │ ├── Orgchart │ │ │ ├── Orgchart.module.js │ │ │ ├── OrgchartService.js │ │ │ ├── Orgchart.vue │ │ │ └── OrgchartBody.vue │ │ ├── Dashboard │ │ │ ├── Dashboard.module.js │ │ │ └── Dashboard.vue │ │ ├── Auth │ │ │ ├── PasswordReset │ │ │ │ ├── RequestPasswordResetModel.js │ │ │ │ └── ResetPasswordModel.js │ │ │ ├── LoginModel.js │ │ │ └── RegisterModel.js │ │ └── User │ │ │ ├── PasswordResetModel.js │ │ │ └── UserModel.js │ ├── constants.js │ ├── shared │ │ ├── AppSettings.js │ │ └── i18n.js │ ├── icons.js │ ├── main.js │ └── App.vue ├── babel.config.js ├── public │ ├── favicon.ico │ ├── assets │ │ ├── logo.png │ │ └── img │ │ │ ├── apollo11-white.png │ │ │ ├── product_logo.png │ │ │ └── avatar.svg │ └── index.html ├── .env.example ├── .gitignore └── Dockerfile ├── runtime └── .gitignore ├── web ├── assets │ └── .gitignore ├── robots.txt ├── storage │ └── .gitignore ├── favicon.ico ├── .htaccess ├── index.php └── index-test.php ├── .bowerrc ├── vagrant ├── config │ ├── .gitignore │ └── vagrant-local.example.yml ├── nginx │ ├── log │ │ └── .gitignore │ └── app.conf └── provision │ ├── always-as-root.sh │ └── once-as-vagrant.sh ├── env.php ├── config ├── params.php ├── test_db.php ├── db.php ├── test.php ├── common.php └── console.php ├── migrate ├── modules └── v1 │ ├── users │ ├── UserModule.php │ ├── resources │ │ └── UserProfileResource.php │ └── controllers │ │ └── UserController.php │ ├── workspaces │ ├── WorkspaceModule.php │ ├── controllers │ │ ├── UserLikeController.php │ │ ├── UserCommentController.php │ │ └── WorkspaceActivityController.php │ ├── resources │ │ ├── WorkspaceActivityResource.php │ │ ├── UserLikeResource.php │ │ ├── UserWorkspaceResource.php │ │ ├── UserCommentResource.php │ │ └── FolderResource.php │ ├── models │ │ └── query │ │ │ ├── WorkspaceActivityQuery.php │ │ │ ├── TimelinePostQuery.php │ │ │ ├── ArticleQuery.php │ │ │ ├── WorkspaceQuery.php │ │ │ ├── UserLikeQuery.php │ │ │ ├── UserWorkspaceQuery.php │ │ │ └── UserCommentQuery.php │ └── workspaceBehaviours │ │ └── UrlAnchorBehaviour.php │ ├── V1Module.php │ └── setup │ ├── SetupModule.php │ ├── controllers │ ├── SiteController.php │ ├── CountryController.php │ └── DepartmentController.php │ ├── models │ ├── query │ │ ├── CountryQuery.php │ │ ├── UserDepartmentQuery.php │ │ ├── DepartmentQuery.php │ │ └── InvitationQuery.php │ └── search │ │ └── DepartmentSearch.php │ └── resources │ ├── UserDepartmentResource.php │ └── CountryResource.php ├── rest ├── Controller.php ├── ActiveController.php ├── ValidationException.php ├── ControllerAuthTrait.php └── ControllerTrait.php ├── docker ├── php │ ├── www.conf │ ├── install-composer.sh │ ├── php.ini │ └── Dockerfile └── vhost.conf ├── .env.example ├── views └── site │ ├── about.php │ ├── error.php │ └── login.php ├── mail ├── reset_password.php ├── invitation_accepted.php ├── user_invitation.php └── layouts │ └── html.php ├── .editorconfig ├── .gitignore ├── yii.bat ├── migrations ├── m201220_195317_alter_body_column_on_folders_table.php ├── m200915_131155_add_expire_date_column_to_users_table.php ├── m201120_161339_add_data_column_to_user_activity_table.php ├── m201228_165822_drop_abbreviation_column_from_workspace_table.php ├── m201008_150226_add_access_token_expire_date_column_to_users_table.php ├── m201215_112037_change_workspace_activity_data_column_into_longtext.php ├── m201113_112426_delete_file_path_column_to_timeline_posts_table.php ├── m201214_175912_drop_description_column_from_workspace_activity_table.php ├── m201201_110159_add_parent_identity_column_to_workspace_activity_table.php ├── m201215_111820_drop_parent_identity_column_from_workspace_activity_table.php ├── m201005_132829_add_favourites_columns_to_users_table.php ├── m201013_125848_add_action_column_to_timeline_posts_table.php ├── m200904_114621_add_demo_users.php ├── m201109_113741_alter_table_timeline_posts_add_column_workspace_id.php ├── m201119_094300_create_user_activity_table.php └── m201127_100743_create_workspace_activity_table.php ├── helpers.php ├── autocompletion.php ├── yii ├── assets └── AppAsset.php ├── components └── AsyncComponent.php ├── base └── ConsoleController.php ├── codeception.yml ├── commands └── HelloController.php ├── docker-compose.yml ├── helpers └── ModelHelper.php ├── LICENSE.md └── create-database.php /tests/_data/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vue/src/core/components/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/acceptance/_bootstrap.php: -------------------------------------------------------------------------------- 1 | load(); -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/callouts.scss: -------------------------------------------------------------------------------- 1 | @mixin callout-variant($bg, $border-color){ 2 | border-color: $border-color; 3 | background-color: $bg; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /vue/src/core/components/org-chart/src/lib/lodash.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge' 2 | 3 | export const mergeOptions = (obj, src) => { 4 | return merge(obj, src) 5 | } 6 | -------------------------------------------------------------------------------- /vue/.env.example: -------------------------------------------------------------------------------- 1 | VUE_APP_API_HOST=https://api.agoraintra.net/ 2 | #VUE_APP_API_HOST=https://api.demo.agoraintra.net/ 3 | VUE_APP_TWITTER_FEED_URL=https://twitter.com/test?ref_src=test -------------------------------------------------------------------------------- /vue/src/modules/index.js: -------------------------------------------------------------------------------- 1 | import './Dashboard/Dashboard.module'; 2 | import './setup/setupModule'; 3 | import './Workspace/workspaceModule'; 4 | import './Orgchart/Orgchart.module' 5 | -------------------------------------------------------------------------------- /vue/src/plugins/bootstrap-vue/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import BootstrapVue from 'bootstrap-vue' 4 | 5 | Vue.use(BootstrapVue); 6 | 7 | export default BootstrapVue; -------------------------------------------------------------------------------- /config/params.php: -------------------------------------------------------------------------------- 1 | 'admin@example.com', 5 | 'senderEmail' => 'noreply@example.com', 6 | 'senderName' => 'Example.com mailer', 7 | ]; 8 | -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/tooltips.scss: -------------------------------------------------------------------------------- 1 | /*Author : $arboshiki*/ 2 | @mixin tooltip-variant($bg, $color: #FFF){ 3 | background-color: $bg; 4 | color: $color; 5 | border-color: $bg; 6 | } -------------------------------------------------------------------------------- /vue/src/plugins/vue-clipboard2/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueClipboard from 'vue-clipboard2' 3 | 4 | VueClipboard.config.autoSetContainer = true // add this line 5 | Vue.use(VueClipboard) -------------------------------------------------------------------------------- /vue/src/store/modules/user/getters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @param { UserState } state 5 | * @return { object } 6 | */ 7 | export function getUser(state) { 8 | return state.user; 9 | } 10 | -------------------------------------------------------------------------------- /tests/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Setup

4 |
5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /vue/src/store/modules/employee/state.js: -------------------------------------------------------------------------------- 1 | const STATE = { 2 | modal: { 3 | show: false, 4 | object: {} 5 | }, 6 | 7 | modalDropdownData: {}, 8 | loading: false, 9 | data: { 10 | rows: [] 11 | } 12 | }; 13 | 14 | export default STATE; -------------------------------------------------------------------------------- /vue/src/store/modules/setup/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import mutations from './mutations'; 3 | import state from './state'; 4 | 5 | export default { 6 | namespaced: true, 7 | mutations, 8 | actions, 9 | state, 10 | }; 11 | -------------------------------------------------------------------------------- /vue/src/store/modules/workspaces/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | import mutations from './mutations'; 3 | import state from './state'; 4 | 5 | export default { 6 | namespaced: true, 7 | mutations, 8 | actions, 9 | state, 10 | }; 11 | -------------------------------------------------------------------------------- /modules/v1/users/UserModule.php: -------------------------------------------------------------------------------- 1 | 2 |

Page not found

3 | 4 | 5 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/forms.scss: -------------------------------------------------------------------------------- 1 | /*Author : @arboshiki*/ 2 | @mixin lobi-form-input-validation($color, $border){ 3 | label{ 4 | color: $color; 5 | } 6 | select, 7 | input, 8 | textarea{ 9 | border-color: $border; 10 | } 11 | } -------------------------------------------------------------------------------- /vue/src/store/modules/user/state.js: -------------------------------------------------------------------------------- 1 | /** @var { UserState } */ 2 | const STATE = { 3 | currentUser: { 4 | loading: false, 5 | loaded: false, 6 | data: [] 7 | }, 8 | passwordForm: { 9 | loading: false 10 | } 11 | }; 12 | 13 | export default STATE; 14 | -------------------------------------------------------------------------------- /vue/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import './bootstrap-vue'; 2 | import './font-awesome'; 3 | import './vee-validate'; 4 | import './confirm-dialog'; 5 | import './alert-dialog'; 6 | import './notification'; 7 | import './toaster'; 8 | import './ckeditor'; 9 | import './vue-clipboard2'; 10 | -------------------------------------------------------------------------------- /tests/acceptance/AboutCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/about')); 10 | $I->see('About', 'h1'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rest/Controller.php: -------------------------------------------------------------------------------- 1 | 9 | * @package app\rest 10 | */ 11 | class Controller extends \yii\rest\Controller 12 | { 13 | use ControllerTrait; 14 | } -------------------------------------------------------------------------------- /vue/src/core/services/event-bus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const eventBus = new Vue(); 4 | export const SHARE_TO_TIMELINE = 'SHARE_TO_TIMELINE'; 5 | export const SHARE_ARTICLE = 'SHARE_ARTICLE'; 6 | export const SHARE_FILE = 'SHARE_FILE'; 7 | export const EDIT_TIMELINE = 'EDIT_TIMELINE'; -------------------------------------------------------------------------------- /modules/v1/workspaces/WorkspaceModule.php: -------------------------------------------------------------------------------- 1 | li.active{ 7 | >a{ 8 | color: $panel-bg; 9 | } 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/dropdowns.scss: -------------------------------------------------------------------------------- 1 | /*Author : @arboshiki*/ 2 | @mixin dropdown-variant($color, $bg){ 3 | &~.dropdown-menu{ 4 | li{ 5 | a{ 6 | &:hover{ 7 | color: $color; 8 | background-color: $bg; 9 | } 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /vagrant/provision/always-as-root.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #== Bash helpers == 4 | 5 | function info { 6 | echo " " 7 | echo "--> $1" 8 | echo " " 9 | } 10 | 11 | #== Provision script == 12 | 13 | info "Provision-script user: `whoami`" 14 | 15 | info "Restart web-stack" 16 | service php7.0-fpm restart 17 | service nginx restart 18 | service mysql restart -------------------------------------------------------------------------------- /vue/src/core/components/org-chart/src/index.js: -------------------------------------------------------------------------------- 1 | import VoBasic from '../src/components/VoBasic.vue' 2 | import VoEdit from '../src/components/VoEdit.vue' 3 | 4 | export { 5 | VoBasic, 6 | VoEdit 7 | } 8 | 9 | if (typeof window !== 'undefined' && window.Vue) { 10 | window.Vue.component('vo-basic', VoBasic) 11 | window.Vue.component('vo-edit', VoEdit) 12 | } 13 | -------------------------------------------------------------------------------- /vue/src/icons.js: -------------------------------------------------------------------------------- 1 | import {library} from '@fortawesome/fontawesome-svg-core' 2 | import {faTachometerAlt, faAngleLeft, faAngleRight, faBars ,faPencilAlt} from '@fortawesome/free-solid-svg-icons' 3 | import {faCalendarAlt, faEdit} from '@fortawesome/free-regular-svg-icons' 4 | 5 | library.add(faTachometerAlt, faCalendarAlt, faAngleLeft, faAngleRight, faBars, faEdit,faPencilAlt); 6 | -------------------------------------------------------------------------------- /modules/v1/V1Module.php: -------------------------------------------------------------------------------- 1 | 17 | * @package app\modules\v1 18 | */ 19 | class V1Module extends Module 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | .env.production 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # Others 25 | deploy.sh 26 | deploy-demo.sh -------------------------------------------------------------------------------- /rest/ActiveController.php: -------------------------------------------------------------------------------- 1 | 15 | * @package app\rest 16 | */ 17 | class ActiveController extends \yii\rest\ActiveController 18 | { 19 | use ControllerAuthTrait; 20 | } -------------------------------------------------------------------------------- /modules/v1/setup/SetupModule.php: -------------------------------------------------------------------------------- 1 | 17 | * @package app\modules\v1\setup 18 | */ 19 | class SetupModule extends Module 20 | { 21 | 22 | } -------------------------------------------------------------------------------- /vue/src/modules/Orgchart/Orgchart.module.js: -------------------------------------------------------------------------------- 1 | import MenuService from "../../core/components/sidebar/MenuService"; 2 | import MenuItem from "../../core/components/sidebar/MenuItem"; 3 | import i18n from './../../shared/i18n' 4 | 5 | // MenuService.addItem(new MenuItem('orgchart', { 6 | // text: i18n.t('Org Chart'), 7 | // path: '/orgchart', 8 | // weight: 2, 9 | // icon: 'fas fa-sitemap' 10 | // })); 11 | -------------------------------------------------------------------------------- /tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for functional (integration) tests. 4 | # emulate web requests and make application process them. 5 | # (tip: better to use with frameworks). 6 | 7 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 8 | #basic/web/index.php 9 | class_name: FunctionalTester 10 | modules: 11 | enabled: 12 | - Filesystem 13 | - Yii2 14 | -------------------------------------------------------------------------------- /config/db.php: -------------------------------------------------------------------------------- 1 | 'yii\db\Connection', 5 | 'dsn' => env('DB_DSN'), 6 | 'username' => env('DB_USERNAME'), 7 | 'password' => env('DB_PASSWORD'), 8 | 'charset' => env('DB_CHARSET'), 9 | 10 | // Schema cache options (for production environment) 11 | //'enableSchemaCache' => true, 12 | //'schemaCacheDuration' => 60, 13 | //'schemaCache' => 'cache', 14 | ]; 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORTAL_HOST = http://localhost:8080 2 | API_HOST = http://agora-api.localhost 3 | 4 | DB_DSN = mysql:host=db;port=3306;dbname=yii2_agora 5 | DB_USERNAME = yii2_agora 6 | DB_PASSWORD = 123456 7 | DB_CHARSET = utf8mb4 8 | 9 | EMAIL_FROM = noreply@example.com 10 | EMAIL_CC = 11 | EMAIL_BCC = 12 | 13 | MAILER_FILE_TRANSPORT = false 14 | SMTP_HOST = 15 | SMTP_USERNAME = 16 | SMTP_PASSWORD = 17 | SMTP_PORT = 18 | SMTP_ENCRYPTION = -------------------------------------------------------------------------------- /vue/src/plugins/vee-validate/custom-rules.js: -------------------------------------------------------------------------------- 1 | import {extend} from "vee-validate"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | extend('password', { 5 | validate(value) { 6 | if (!value) return true; 7 | const match = value.match(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/); 8 | return !!match; 9 | }, 10 | message: i18n.t('Should contain at least 1 lower case, 1 upper case, 1 digit and min length of 8') 11 | }); -------------------------------------------------------------------------------- /vue/src/store/setup/getters.js: -------------------------------------------------------------------------------- 1 | 2 | const getTree = (departments, parentId = null) => { 3 | const nodes = []; 4 | for (const item of departments) { 5 | if (item.parent_id === parentId) { 6 | item.children = getTree(departments, item.id); 7 | nodes.push(item); 8 | } 9 | } 10 | 11 | return nodes 12 | } 13 | 14 | export function departmentsTree(state) { 15 | return getTree(state.departments.data, null) 16 | } 17 | -------------------------------------------------------------------------------- /vue/src/core/services/dateService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by zura on 5/23/18. 3 | */ 4 | import moment from 'moment' 5 | 6 | export const DATE_FORMATS = { 7 | USER_DATE_FORMAT: 'LL', 8 | USER_DATETIME_FORMAT: 'LLL' 9 | } 10 | 11 | export function convertDateForBackend (date) { 12 | return moment(date).format() 13 | } 14 | export function convertToUserDate (date) { 15 | return moment(date).format(DATE_FORMATS.USER_DATE_FORMAT) 16 | } 17 | -------------------------------------------------------------------------------- /vue/src/core/components/PageFooter.vue: -------------------------------------------------------------------------------- 1 | 6 | 11 | 17 | -------------------------------------------------------------------------------- /views/site/about.php: -------------------------------------------------------------------------------- 1 | title = 'About'; 8 | $this->params['breadcrumbs'][] = $this->title; 9 | ?> 10 |
11 |

title) ?>

12 | 13 |

14 | This is the About page. You may modify the following file to customize its content: 15 |

16 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /vue/src/plugins/vee-validate/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import {ValidationProvider, ValidationObserver, extend} from 'vee-validate'; 3 | import * as rules from 'vee-validate/dist/rules'; 4 | import "./custom-rules"; 5 | 6 | Vue.component('ValidationProvider', ValidationProvider); 7 | Vue.component('ValidationObserver', ValidationObserver); 8 | 9 | Object.keys(rules).forEach(rule => { 10 | extend(rule, { 11 | ...rules[rule], 12 | }); 13 | }); -------------------------------------------------------------------------------- /vue/src/core/filters/dateFilter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import moment from 'moment'; 3 | import i18n from "../../shared/i18n"; 4 | 5 | Vue.filter('toDatetime', function (value) { 6 | if (!value) return ''; 7 | return moment(value).format('YYYY-MM-DD HH:mm:ss'); 8 | }); 9 | 10 | Vue.filter('relativeDate', function (value) { 11 | if (!value) return ''; 12 | moment.locale(i18n.locale); 13 | return moment.tz(value, 'Asia/Tbilisi').fromNow() 14 | }); -------------------------------------------------------------------------------- /modules/v1/workspaces/controllers/UserLikeController.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/index')); 10 | $I->see('My Company'); 11 | 12 | $I->seeLink('About'); 13 | $I->click('About'); 14 | $I->wait(2); // wait for page to be opened 15 | 16 | $I->see('This is the About page.'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vue/src/core/scss/bootstrap/override/nav.scss: -------------------------------------------------------------------------------- 1 | .nav-pills { 2 | background-color: #FFF; 3 | border-top: 1px solid #dbdbdb; 4 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.1); 5 | 6 | .nav-link { 7 | border-bottom: 2px solid transparent; 8 | border-top: 2px solid transparent; 9 | transition: all 0.3s; 10 | 11 | &.active { 12 | color: #3989c6; 13 | background-color: #ffffff; 14 | border-bottom: 2px solid #3989c6; 15 | border-radius: 0; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | run(); 16 | -------------------------------------------------------------------------------- /mail/reset_password.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |

username ?>,

11 | 12 |

13 | 14 |
15 | password_reset_token") ?> 16 |

17 | 18 | -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/tables.scss: -------------------------------------------------------------------------------- 1 | /*Author : $arboshiki*/ 2 | @mixin table-row-color-variant($state, $color){ 3 | // Exact selectors below required to override `.table-striped` and prevent 4 | // inheritance to nested tables. 5 | .table > thead > tr, 6 | .table > tbody > tr, 7 | .table > tfoot > tr { 8 | > td.#{$state}, 9 | > th.#{$state}, 10 | &.#{$state} > td, 11 | &.#{$state} > th { 12 | color: $color; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vue/src/modules/Dashboard/Dashboard.module.js: -------------------------------------------------------------------------------- 1 | import MenuService from "../../core/components/sidebar/MenuService"; 2 | import MenuItem from "../../core/components/sidebar/MenuItem"; 3 | import i18n from './../../shared/i18n' 4 | 5 | 6 | MenuService.addItem(new MenuItem('main', { 7 | text: i18n.t('Application'), 8 | weight: 0, 9 | isGroup: true, 10 | })); 11 | MenuService.addItem(new MenuItem('dashboard', { 12 | text: i18n.t('Dashboard'), 13 | path: '/dashboard', 14 | weight: 1, 15 | icon: 'fas fa-tachometer-alt' 16 | })); 17 | -------------------------------------------------------------------------------- /vue/src/store/modules/employee/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_DATA = 'employee/SET_DATA'; 2 | export const SET_MODAL_DROPDOWN_DATA = 'employee/SET_MODAL_DROPDOWN_DATA'; 3 | export const SHOW_EMPLOYEE_MODAL = 'employee/SHOW_EMPLOYEE_MODAL'; 4 | export const CHANGE_LOADING = 'employee/CHANGE_LOADING'; 5 | export const HIDE_MODAL = 'employee/HIDE_MODAL' 6 | export const DELETED_USER = 'employee/DELETED_USER' 7 | export const CHANGE_USER_ROLE = 'employee/CHANGE_USER_ROLE' 8 | export const REMOVE_USER_FROM_WORKSPACE = 'employee/REMOVE_USER_FROM_WORKSPACE' 9 | -------------------------------------------------------------------------------- /vue/src/plugins/notification.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Notifications from 'vue-notification' 3 | import i18n from "@/shared/i18n"; 4 | 5 | Vue.use(Notifications); 6 | 7 | 8 | const SuccessNotification = { 9 | install(Vue) { 10 | Vue.prototype.$successToast = function (message, title = i18n.t('Success')) { 11 | this.$notify({ 12 | group: 'success', 13 | type: 'success', 14 | title, 15 | text: message, 16 | }); 17 | } 18 | } 19 | }; 20 | 21 | Vue.use(SuccessNotification); 22 | -------------------------------------------------------------------------------- /vue/src/modules/Auth/PasswordReset/RequestPasswordResetModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../../shared/i18n'; 3 | 4 | export default class RequestPasswordResetModel extends BaseModel { 5 | email = null; 6 | 7 | rules = { 8 | email: [ 9 | {rule: 'required'}, 10 | {rule: 'email'}, 11 | ] 12 | }; 13 | 14 | attributeLabels = { 15 | email: i18n.t('Email'), 16 | }; 17 | 18 | constructor(email = '') { 19 | super(); 20 | this.email = email; 21 | } 22 | } -------------------------------------------------------------------------------- /vue/src/modules/setup/countries/CountryModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | export default class CountryModel extends BaseModel { 5 | name = null; 6 | 7 | rules = { 8 | name: 'required' 9 | }; 10 | 11 | attributeLabels = { 12 | name: i18n.t('Country Name'), 13 | }; 14 | 15 | constructor(data = {}) { 16 | super(); 17 | Object.assign(this, {...data}); 18 | } 19 | 20 | toJSON() { 21 | return { 22 | name: this.name 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /vue/src/modules/setup/invitations/UserInvitationFormModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../../shared/i18n'; 3 | 4 | export default class UserInvitationFormModel extends BaseModel { 5 | email = null; 6 | 7 | rules = { 8 | email: [ 9 | {rule: 'required'}, 10 | {rule: 'email'}, 11 | ], 12 | }; 13 | 14 | attributeLabels = { 15 | email: i18n.t('Email'), 16 | }; 17 | 18 | constructor(email = '') { 19 | super(); 20 | this.email = email; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [*.less] 13 | indent_size = 2 14 | 15 | [*.sass] 16 | indent_size = 2 17 | 18 | [*.scss] 19 | indent_size = 2 20 | 21 | [*.vue] 22 | indent_size = 2 23 | tab_width = 2 24 | 25 | [{*.cjs,*.js}] 26 | indent_size = 2 27 | tab_width = 2 28 | 29 | [{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,composer.lock,jest.config}] 30 | indent_size = 2 31 | -------------------------------------------------------------------------------- /web/index-test.php: -------------------------------------------------------------------------------- 1 | run(); 17 | -------------------------------------------------------------------------------- /vue/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | import './core/index'; 5 | 6 | import router from './shared/router'; 7 | import i18n from './shared/i18n'; 8 | 9 | import './modules'; 10 | import './plugins'; 11 | import store from './store'; 12 | import './index.scss'; 13 | 14 | import VueMoment from 'vue-moment'; 15 | import moment from 'moment-timezone' 16 | 17 | Vue.use(VueMoment, {moment}); 18 | Vue.config.productionTip = false; 19 | 20 | new Vue({ 21 | i18n, 22 | store, 23 | router, 24 | render: h => h(App) 25 | }).$mount('#app'); 26 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/invite/WorkspaceInviteModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | export default class WorkspaceInviteModel extends BaseModel { 5 | selectedUsers = []; 6 | allUser = false; 7 | role = 'user'; 8 | 9 | rules = {}; 10 | 11 | attributeLabels = { 12 | selectedUsers: i18n.t('Users'), 13 | allUser: i18n.t('Select all registered users'), 14 | role: i18n.t('Role'), 15 | }; 16 | 17 | constructor(data = {}) { 18 | super(); 19 | Object.assign(this, {...data}); 20 | } 21 | } -------------------------------------------------------------------------------- /vue/src/modules/Workspace/view/files/FolderFormModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../../core/components/input-widget/BaseModel"; 2 | import i18n from "../../../../shared/i18n"; 3 | 4 | export default class FolderFormModel extends BaseModel { 5 | name = ''; 6 | 7 | rules = { 8 | name: 'required', 9 | }; 10 | 11 | attributeLabels = { 12 | name: i18n.t('Name'), 13 | }; 14 | 15 | constructor(data = {}) { 16 | super(); 17 | data.created_at = data.created_at / 1000; 18 | data.updated_at = data.updated_at / 1000; 19 | Object.assign(this, {...data}); 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # phpstorm project files 2 | .idea 3 | 4 | # netbeans project files 5 | nbproject 6 | 7 | # zend studio for eclipse project files 8 | .buildpath 9 | .project 10 | .settings 11 | 12 | # windows thumbnail cache 13 | Thumbs.db 14 | 15 | # composer vendor dir 16 | /vendor 17 | 18 | # composer itself is not needed 19 | composer.phar 20 | 21 | # Mac DS_Store Files 22 | .DS_Store 23 | 24 | # phpunit itself is not needed 25 | phpunit.phar 26 | # local phpunit config 27 | /phpunit.xml 28 | 29 | tests/_output/* 30 | tests/_support/_generated 31 | 32 | #vagrant folder 33 | /.vagrant 34 | 35 | .env -------------------------------------------------------------------------------- /yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem ------------------------------------------------------------- 4 | rem Yii command line bootstrap script for Windows. 5 | rem 6 | rem @author Qiang Xue 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | -------------------------------------------------------------------------------- /docker/php/install-composer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) 4 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 5 | ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") 6 | 7 | if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] 8 | then 9 | >&2 echo 'ERROR: Invalid installer signature' 10 | rm composer-setup.php 11 | exit 1 12 | fi 13 | 14 | php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer 15 | RESULT=$? 16 | rm composer-setup.php 17 | exit $RESULT 18 | -------------------------------------------------------------------------------- /tests/bin/yii.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem ------------------------------------------------------------- 4 | rem Yii command line bootstrap script for Windows. 5 | rem 6 | rem @author Qiang Xue 7 | rem @link http://www.yiiframework.com/ 8 | rem @copyright Copyright (c) 2008 Yii Software LLC 9 | rem @license http://www.yiiframework.com/license/ 10 | rem ------------------------------------------------------------- 11 | 12 | @setlocal 13 | 14 | set YII_PATH=%~dp0 15 | 16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe 17 | 18 | "%PHP_COMMAND%" "%YII_PATH%yii" %* 19 | 20 | @endlocal 21 | -------------------------------------------------------------------------------- /vue/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | # install simple http server for serving static content 4 | RUN npm install -g http-server 5 | 6 | # make the 'app' folder the current working directory 7 | WORKDIR /app 8 | 9 | # copy both 'package.json' and 'package-lock.json' (if available) 10 | COPY package*.json ./ 11 | 12 | # install project dependencies 13 | RUN npm install 14 | 15 | # copy project files and folders to the current working directory (i.e. 'app' folder) 16 | COPY . . 17 | 18 | # build app for production with minification 19 | RUN npm run build 20 | 21 | EXPOSE 8080 22 | CMD [ "http-server", "dist" ] -------------------------------------------------------------------------------- /vagrant/config/vagrant-local.example.yml: -------------------------------------------------------------------------------- 1 | # Your personal GitHub token 2 | github_token: 3 | # Read more: https://github.com/blog/1509-personal-api-tokens 4 | # You can generate it here: https://github.com/settings/tokens 5 | 6 | # Guest OS timezone 7 | timezone: Europe/London 8 | 9 | # Are we need check box updates for every 'vagrant up'? 10 | box_check_update: false 11 | 12 | # Virtual machine name 13 | machine_name: yii2basic 14 | 15 | # Virtual machine IP 16 | ip: 192.168.83.137 17 | 18 | # Virtual machine CPU cores number 19 | cpus: 1 20 | 21 | # Virtual machine RAM 22 | memory: 1024 23 | -------------------------------------------------------------------------------- /migrations/m201220_195317_alter_body_column_on_folders_table.php: -------------------------------------------------------------------------------- 1 | renameColumn('{{%folders}}', 'body', 'data'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->renameColumn('{{%folders}}', 'data', 'body'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue/src/core/components/NoDataAvailable.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 26 | -------------------------------------------------------------------------------- /vue/src/store/modules/setup/state.js: -------------------------------------------------------------------------------- 1 | const STATE = { 2 | countries: { 3 | loading: false, 4 | loaded: false, 5 | data: [] 6 | }, 7 | countryModal: { 8 | loading: false, 9 | show: false, 10 | data: {} 11 | }, 12 | departments: { 13 | loading: false, 14 | loaded: false, 15 | data: [] 16 | }, 17 | departmentModal: { 18 | loading: false, 19 | show: false, 20 | data: {} 21 | }, 22 | invitations: { 23 | loading: false, 24 | data: {} 25 | }, 26 | invitationModal: { 27 | loading: false, 28 | show: false, 29 | data: {} 30 | } 31 | }; 32 | 33 | export default STATE; 34 | -------------------------------------------------------------------------------- /migrations/m200915_131155_add_expire_date_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%users}}', 'expire_date', $this->integer(11)->after('password_reset_token')); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->dropColumn('{{%users}}', 'expire_date'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/m201120_161339_add_data_column_to_user_activity_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%user_activity}}', 'data', $this->text()->after('description')); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->dropColumn('{{%user_activity}}', 'data'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/v1/users/resources/UserProfileResource.php: -------------------------------------------------------------------------------- 1 | 17 | * @package app\modules\v1\setup\resources 18 | */ 19 | class UserProfileResource extends User 20 | { 21 | public function fields() 22 | { 23 | return array_merge(parent::fields(), [ 24 | 'access_token', 'mobile', 'phone', 'birthday', 'about_me', 'hobbies', 25 | ]); 26 | } 27 | } -------------------------------------------------------------------------------- /views/site/error.php: -------------------------------------------------------------------------------- 1 | title = $name; 11 | ?> 12 |
13 | 14 |

title) ?>

15 | 16 |
17 | 18 |
19 | 20 |

21 | The above error occurred while the Web server was processing your request. 22 |

23 |

24 | Please contact us if you think this is a server error. Thank you. 25 |

26 | 27 |
28 | -------------------------------------------------------------------------------- /migrations/m201228_165822_drop_abbreviation_column_from_workspace_table.php: -------------------------------------------------------------------------------- 1 | dropColumn('{{%workspaces}}', 'abbreviation'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->addColumn('{{%workspaces}}', 'abbreviation', $this->string(55)->after('name')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/acceptance/LoginCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/login')); 10 | $I->see('Login', 'h1'); 11 | 12 | $I->amGoingTo('try to login with correct credentials'); 13 | $I->fillField('input[name="LoginForm[username]"]', 'admin'); 14 | $I->fillField('input[name="LoginForm[password]"]', 'admin'); 15 | $I->click('login-button'); 16 | $I->wait(2); // wait for button to be clicked 17 | 18 | $I->expectTo('see user info'); 19 | $I->see('Logout'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | user->getId(); 9 | } 10 | 11 | /** 12 | * @param string $key 13 | * @param mixed $default 14 | * @return mixed 15 | */ 16 | function env($key, $default = null) 17 | { 18 | 19 | $value = $_ENV[$key] ?? $_SERVER[$key]; 20 | 21 | if ($value === false) { 22 | return $default; 23 | } 24 | 25 | switch (strtolower($value)) { 26 | case 'true': 27 | case '(true)': 28 | return true; 29 | 30 | case 'false': 31 | case '(false)': 32 | return false; 33 | } 34 | 35 | return $value; 36 | } -------------------------------------------------------------------------------- /vue/src/core/components/sidebar/MenuService.js: -------------------------------------------------------------------------------- 1 | import MenuItem from './MenuItem'; 2 | import store from '../../../store'; 3 | 4 | class MenuService { 5 | 6 | addItem(menuItem) { 7 | if (!(menuItem instanceof MenuItem)) { 8 | throw new Error("addMenuItem accepts MenuItem class instance only") 9 | } 10 | store.dispatch('addMenuItem', { 11 | name: menuItem.name, 12 | menuItem 13 | }); 14 | } 15 | 16 | removeItem(name) { 17 | store.dispatch('removeMenuItem', name); 18 | } 19 | 20 | getItems() { 21 | return store.getters['menuItems'] 22 | } 23 | } 24 | 25 | const menuService = new MenuService(); 26 | 27 | export default menuService; 28 | -------------------------------------------------------------------------------- /tests/_support/FunctionalTester.php: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /vue/src/core/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | @import "./mixins.scss"; 3 | @import "./main.scss"; 4 | 5 | .hover-pointer { 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | } 10 | 11 | .shrinked-width { 12 | max-width: 680px; 13 | margin: 0 auto; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 8px; 18 | } 19 | 20 | /* Track */ 21 | ::-webkit-scrollbar-track { 22 | background-color: #e3e3e3; 23 | border-radius: 10px; 24 | } 25 | 26 | /* Handle */ 27 | ::-webkit-scrollbar-thumb { 28 | background: rgba(0, 0, 0, 0.2); 29 | border-radius: 10px; 30 | } 31 | 32 | /* Handle on hover */ 33 | ::-webkit-scrollbar-thumb:hover { 34 | background: rgba(0, 0, 0, 0.4);; 35 | } -------------------------------------------------------------------------------- /autocompletion.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Yii extends \yii\BaseYii 14 | { 15 | /** 16 | * @var BaseApplication|\yii\web\Application|\yii\base\Application|\yii\console\Application 17 | */ 18 | public static $app; 19 | } 20 | 21 | /** 22 | * Class BaseApplication 23 | * 24 | * @property \app\components\AsyncComponent $async 25 | * @author Zura Sekhniashvili 26 | */ 27 | abstract class BaseApplication extends yii\base\Application 28 | { 29 | 30 | } -------------------------------------------------------------------------------- /mail/invitation_accepted.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |

14 | $model->createdBy->email 17 | ]) 18 | ?> 19 |

20 |

21 | $model->email, 24 | ]) 25 | ?> 26 |

27 |

28 | 29 |

30 | -------------------------------------------------------------------------------- /migrations/m201008_150226_add_access_token_expire_date_column_to_users_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%users}}', 'access_token_expire_date', $this->integer(11)->after('access_token')); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->dropColumn('{{%users}}', 'access_token_expire_date'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/m201215_112037_change_workspace_activity_data_column_into_longtext.php: -------------------------------------------------------------------------------- 1 | alterColumn('{{%workspace_activity}}', 'data', 'LONGTEXT'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->alterColumn('{{%workspace_activity}}', 'data', $this->text()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue/src/modules/Auth/LoginModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../shared/i18n'; 3 | 4 | export default class LoginModel extends BaseModel { 5 | email = null; 6 | password = null; 7 | 8 | rules = { 9 | email: [ 10 | {rule: 'required'}, 11 | {rule: 'email', message: i18n.t('This must be valid email')}, 12 | ], 13 | password: 'required', 14 | }; 15 | 16 | attributeLabels = { 17 | email: i18n.t('Email'), 18 | password: i18n.t('Password') 19 | }; 20 | 21 | constructor(email = '', password = '') { 22 | super(); 23 | this.email = email; 24 | this.password = password; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/view/articles/ArticleFormModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "../../../../shared/i18n"; 3 | 4 | export default class ArticleFormModel extends BaseModel { 5 | id = null; 6 | title = ''; 7 | workspace_id = null; 8 | body = ''; 9 | 10 | rules = { 11 | title: 'required', 12 | body: '', 13 | }; 14 | 15 | attributeLabels = { 16 | title: i18n.t('Title'), 17 | body: i18n.t('Body'), 18 | }; 19 | 20 | constructor(data = {}) { 21 | super(); 22 | data.created_at = data.created_at / 1000; 23 | data.updated_at = data.updated_at / 1000; 24 | Object.assign(this, {...data}); 25 | } 26 | } -------------------------------------------------------------------------------- /vue/src/modules/setup/employees/RoleModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from "../../../shared/i18n"; 3 | 4 | export default class RoleModel extends BaseModel { 5 | id = null; 6 | role = ''; 7 | workspace_id = null; 8 | 9 | rules = { 10 | id: [ 11 | {rule: 'required'} 12 | ], 13 | role: [ 14 | {rule: 'required'} 15 | ], 16 | workspace_id: [ 17 | {rule: 'required'} 18 | ] 19 | } 20 | 21 | attributeLabels = { 22 | role: i18n.t('Role'), 23 | workspace_id: i18n.t('Workspace'), 24 | }; 25 | 26 | constructor(data) { 27 | super(); 28 | Object.assign(this, {...data}) 29 | } 30 | } -------------------------------------------------------------------------------- /migrations/m201113_112426_delete_file_path_column_to_timeline_posts_table.php: -------------------------------------------------------------------------------- 1 | dropColumn('{{%timeline_posts}}', 'file_path'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->addColumn('{{%timeline_posts}}', 'file_path', $this->string(1024)->after('description')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/m201214_175912_drop_description_column_from_workspace_activity_table.php: -------------------------------------------------------------------------------- 1 | dropColumn('{{%workspace_activity}}', 'description'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->addColumn('{{%workspace_activity}}', 'description', 'LONGTEXT AFTER `action`'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue/src/core/filters/bytesFilter.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | Vue.filter('prettyBytes', function (num) { 4 | if (typeof num !== 'number' || isNaN(num)) { 5 | return '-'; 6 | } 7 | 8 | let exponent; 9 | let unit; 10 | let neg = num < 0; 11 | let units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; 12 | 13 | if (neg) { 14 | num = -num; 15 | } 16 | 17 | if (num < 1) { 18 | return (neg ? '-' : '') + num + ' B'; 19 | } 20 | 21 | exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), units.length - 1); 22 | num = (num / Math.pow(1000, exponent)).toFixed(1) * 1; 23 | unit = units[exponent]; 24 | 25 | return (neg ? '-' : '') + num + ' ' + unit; 26 | }); -------------------------------------------------------------------------------- /yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 24 | exit($exitCode); 25 | -------------------------------------------------------------------------------- /migrations/m201201_110159_add_parent_identity_column_to_workspace_activity_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%workspace_activity}}', 'parent_identity', $this->text()->after('data')); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->dropColumn('{{%workspace_activity}}', 'parent_identity'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue/src/store/modules/user/mutations.js: -------------------------------------------------------------------------------- 1 | import {SET_USER, SET_PROFILE_LOADING, SET_PASSWORD_FORM_LOADING} from './mutation-types'; 2 | 3 | export default { 4 | /** 5 | * 6 | * @param { UserState } state 7 | * @param { array } userProfile 8 | */ 9 | [SET_USER](state, userProfile) { 10 | state.currentUser.data = userProfile; 11 | state.currentUser.loaded = true; 12 | }, 13 | 14 | /** 15 | * 16 | * @param { UserState } state 17 | * @param { boolean } loading 18 | */ 19 | [SET_PROFILE_LOADING](state, loading) { 20 | state.currentUser.loading = loading; 21 | }, 22 | [SET_PASSWORD_FORM_LOADING](state, loading) { 23 | state.passwordForm.loading = loading; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /rest/ValidationException.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |

$model->email]) ?>

11 |

12 | Yii::$app->name, 14 | 'inviterName' => $model->createdBy->getDisplayName() 15 | ]) ?> 16 |

17 |

18 | 19 |
20 | token") ?> 21 |

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /migrations/m201215_111820_drop_parent_identity_column_from_workspace_activity_table.php: -------------------------------------------------------------------------------- 1 | dropColumn('{{%workspace_activity}}', 'parent_identity'); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown() 22 | { 23 | $this->addColumn('{{%workspace_activity}}', 'parent_identity', $this->text()->after('data')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue/src/modules/Auth/PasswordReset/ResetPasswordModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../../shared/i18n'; 3 | 4 | export default class ResetPasswordModel extends BaseModel { 5 | password = ''; 6 | repeat_password = ''; 7 | token = ''; 8 | 9 | rules = { 10 | password: 'required', 11 | repeat_password: [ 12 | {rule: 'required'}, 13 | {rule: 'confirmed', target: 'password'}, 14 | ], 15 | }; 16 | 17 | attributeLabels = { 18 | password: i18n.t('Password'), 19 | repeat_password: i18n.t('Repeat password'), 20 | }; 21 | 22 | constructor(password = '') { 23 | super(); 24 | this.password = password; 25 | } 26 | } -------------------------------------------------------------------------------- /assets/AppAsset.php: -------------------------------------------------------------------------------- 1 | 16 | * @since 2.0 17 | */ 18 | class AppAsset extends AssetBundle 19 | { 20 | public $basePath = '@webroot'; 21 | public $baseUrl = '@web'; 22 | public $css = [ 23 | 'css/site.css', 24 | ]; 25 | public $js = [ 26 | ]; 27 | public $depends = [ 28 | 'yii\web\YiiAsset', 29 | 'yii\bootstrap\BootstrapAsset', 30 | ]; 31 | } 32 | -------------------------------------------------------------------------------- /components/AsyncComponent.php: -------------------------------------------------------------------------------- 1 | 15 | * @package app\components 16 | */ 17 | class AsyncComponent extends \yii\base\Component 18 | { 19 | public string $phpBinary = 'php'; 20 | 21 | public function execute($action, ...$args) 22 | { 23 | $command = $this->phpBinary . ' ' . \Yii::getAlias('@app/yii') . ' ' . $action . ' ' . implode(" ", $args).' >> /dev/null &'; 24 | \Yii::info("START executing command \"$command\""); 25 | exec($command, $output); 26 | } 27 | } -------------------------------------------------------------------------------- /modules/v1/workspaces/resources/WorkspaceActivityResource.php: -------------------------------------------------------------------------------- 1 | function($model) { return Json::decode($model->data); }, 17 | 'createdBy' 18 | ]); 19 | } 20 | 21 | public function getCreatedBy() 22 | { 23 | return $this->hasOne(UserResource::class, ['id' => 'created_by']); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/_support/UnitTester.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Agora - Open source Social intranet 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /base/ConsoleController.php: -------------------------------------------------------------------------------- 1 | 17 | * @package app\base 18 | */ 19 | class ConsoleController extends Controller 20 | { 21 | public string $dateFormat = 'Y-m-d H:i:s'; 22 | 23 | protected function log($message) 24 | { 25 | Console::output(date($this->dateFormat) . ' - ' . $message); 26 | } 27 | 28 | protected function error($message) 29 | { 30 | Console::error(date($this->dateFormat) . ' - ' . $message); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | 1e10){ 22 | data.created_at = data.created_at / 1000; 23 | data.updated_at = data.updated_at / 1000; 24 | } 25 | Object.assign(this, {...data}); 26 | } 27 | } -------------------------------------------------------------------------------- /vue/src/core/components/NoData.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | -------------------------------------------------------------------------------- /migrations/m201005_132829_add_favourites_columns_to_users_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%users}}', 'favourites', $this->getDb()->getSchema()->createColumnSchemaBuilder('longtext')->after('status')); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function safeDown() 24 | { 25 | $this->dropColumn('{{%users}}', 'favourites'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/v1/workspaces/resources/UserLikeResource.php: -------------------------------------------------------------------------------- 1 | hasOne(UserResource::class, ['id' => 'created_by']); 31 | } 32 | } -------------------------------------------------------------------------------- /docker/php/php.ini: -------------------------------------------------------------------------------- 1 | ; General settings 2 | date.timezone = UTC 3 | memory_limit=-1 4 | max_execution_time=30 5 | sys_temp_dir=/tmp 6 | upload_max_filesize=512M 7 | upload_tmp_dir=/tmp 8 | post_max_size=512M 9 | 10 | ; Security, Debug & Logs 11 | expose_php=off 12 | cgi.fix_pathinfo=0 13 | log_errors=on 14 | error_reporting=E_ALL 15 | html_errors=on 16 | xdebug.default_enable=off 17 | 18 | ; Opcache 19 | opcache.memory_consumption=128 20 | opcache.interned_strings_buffer=8 21 | opcache.max_accelerated_files=4000 22 | ;opcache.validate_timestamps=off 23 | opcache.fast_shutdown=0 24 | opcache.enable_cli=1 25 | 26 | ; PHP language options 27 | short_open_tag=0 28 | 29 | ; xdebug 30 | zend_extension = xdebug.so 31 | xdebug.idekey = PHPSTORM 32 | xdebug.remote_enable = 1 33 | xdebug.remote_connect_back = 1 -------------------------------------------------------------------------------- /vue/src/modules/User/PasswordResetModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | export default class PasswordResetModel extends BaseModel { 5 | old_password = ''; 6 | password = ''; 7 | confirm_password = ''; 8 | 9 | rules = { 10 | old_password: 'required', 11 | password: ['required', 'password'], 12 | confirm_password: [ 13 | 'required', 14 | {rule: 'confirmed', target: 'password'}, 15 | ], 16 | }; 17 | 18 | attributeLabels = { 19 | old_password: i18n.t('Old Password'), 20 | password: i18n.t('Password'), 21 | confirm_password: i18n.t('Confirm Password'), 22 | }; 23 | 24 | constructor(data = {}) { 25 | super(); 26 | Object.assign(this, {...data}); 27 | } 28 | } -------------------------------------------------------------------------------- /vue/src/core/components/PhotoVideoInput.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /mail/layouts/html.php: -------------------------------------------------------------------------------- 1 | 8 | beginPage() ?> 9 | 10 | 11 | 12 | 13 | <?= Html::encode($this->title) ?> 14 | head() ?> 15 | 16 | 17 | beginBody() ?> 18 | 19 | endBody() ?> 20 | 21 | 22 | endPage() ?> 23 | -------------------------------------------------------------------------------- /vagrant/provision/once-as-vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | #== Import script args == 4 | 5 | github_token=$(echo "$1") 6 | 7 | #== Bash helpers == 8 | 9 | function info { 10 | echo " " 11 | echo "--> $1" 12 | echo " " 13 | } 14 | 15 | #== Provision script == 16 | 17 | info "Provision-script user: `whoami`" 18 | 19 | info "Configure composer" 20 | composer config --global github-oauth.github.com ${github_token} 21 | echo "Done!" 22 | 23 | info "Install project dependencies" 24 | cd /app 25 | composer --no-progress --prefer-dist install 26 | 27 | info "Create bash-alias 'app' for vagrant user" 28 | echo 'alias app="cd /app"' | tee /home/vagrant/.bash_aliases 29 | 30 | info "Enabling colorized prompt for guest console" 31 | sed -i "s/#force_color_prompt=yes/force_color_prompt=yes/" /home/vagrant/.bashrc 32 | -------------------------------------------------------------------------------- /vue/src/modules/setup/countries/departments/DepartmentModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | export default class DepartmentModel extends BaseModel { 5 | name = null; 6 | country_id = null; 7 | parent_id = null; 8 | 9 | rules = { 10 | name: 'required', 11 | country_id: 'required', 12 | }; 13 | 14 | attributeLabels = { 15 | name: i18n.t('Department Name'), 16 | country_id: i18n.t('Country'), 17 | parent_id: i18n.t('Parent Department'), 18 | }; 19 | 20 | constructor(data = {}) { 21 | super(); 22 | Object.assign(this, {...data}); 23 | } 24 | 25 | toJSON() { 26 | return { 27 | name: this.name, 28 | country_id: this.country_id, 29 | parent_id: this.parent_id, 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/popovers.scss: -------------------------------------------------------------------------------- 1 | /*Author : @arboshiki*/ 2 | 3 | @mixin popover-variant($title-color, $title-bg, $border){ 4 | &+.popover{ 5 | border-color: $border; 6 | .popover-title{ 7 | background-color: $title-bg; 8 | color: $title-color; 9 | } 10 | &.right{ 11 | .arrow{ 12 | border-right-color: $border; 13 | } 14 | } 15 | &.left{ 16 | .arrow{ 17 | border-left-color: $border; 18 | } 19 | } 20 | &.top{ 21 | .arrow{ 22 | border-top-color: $border; 23 | } 24 | } 25 | &.bottom{ 26 | .arrow{ 27 | border-bottom-color: $border; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /vue/src/modules/setup/employees/employeesService.js: -------------------------------------------------------------------------------- 1 | import httpService from "../../../core/services/httpService"; 2 | 3 | const employeeService = { 4 | url: 'v1/setup/employee', 5 | 6 | get(params = { 7 | sort: '-created_at', 8 | expand: 'userWorkspaces,userDepartments,userDepartments.department,userDepartments.department.country' 9 | }) { 10 | return httpService.get(this.url, {params}); 11 | }, 12 | getByWorkspace(workspaceId) { 13 | return httpService.get(this.url+'/by-workspace', {params: {workspaceId}}); 14 | }, 15 | getModalDropdownData(params = {expand: 'departments'}) { 16 | return httpService.get(this.url + '/get-dropdown', {params}) 17 | }, 18 | updateUserData(params) { 19 | return httpService.put(this.url + `/${params.id}`, params) 20 | } 21 | } 22 | 23 | export default employeeService; -------------------------------------------------------------------------------- /vue/src/modules/Workspace/view/timeline/TimelineFormModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "@/core/components/input-widget/BaseModel"; 2 | import i18n from "@/shared/i18n"; 3 | 4 | export default class TimelineFormModel extends BaseModel { 5 | workspace_id = null; 6 | article_id = null; 7 | attachment_ids = [] || null; 8 | file = null; 9 | file_url = ''; 10 | description = ''; 11 | action = null; 12 | 13 | rules = { 14 | workspace: [ 15 | {rule: 'required'} 16 | ] 17 | } 18 | 19 | attributeLabels = { 20 | file: i18n.t('Select Image or Video File'), 21 | description: i18n.t('Description'), 22 | } 23 | 24 | constructor(data = {}) { 25 | super(); 26 | data.created_at = data.created_at / 1000; 27 | data.updated_at = data.updated_at / 1000; 28 | Object.assign(this, {...data}); 29 | } 30 | } -------------------------------------------------------------------------------- /modules/v1/setup/controllers/SiteController.php: -------------------------------------------------------------------------------- 1 | getErrorHandler()->exception) === null) { 15 | $exception = new NotFoundHttpException(Yii::t('yii', 'Page not found.')); 16 | } 17 | 18 | if ($exception instanceof \HttpException) { 19 | Yii::$app->response->setStatusCode($exception->getCode()); 20 | } else { 21 | Yii::$app->response->setStatusCode(500); 22 | } 23 | 24 | return $this->asJson(['error' => $exception->getMessage(), 'code' => $exception->getCode()]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vue/src/plugins/confirm-dialog.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import i18n from "@/shared/i18n"; 3 | 4 | const ConfirmModal = { 5 | install(Vue) { 6 | Vue.prototype.$confirm = function (message, title = i18n.t('Please Confirm')) { 7 | return new Promise((resolve, reject) => { 8 | this.$bvModal.msgBoxConfirm(message, { 9 | title: title, 10 | okVariant: 'danger', 11 | okTitle: i18n.t('OK'), 12 | cancelTitle: i18n.t('Cancel'), 13 | footerClass: 'p-2', 14 | hideHeaderClose: false, 15 | centered: true 16 | }) 17 | .then(value => { 18 | resolve(value) 19 | }) 20 | .catch(err => { 21 | reject(err); 22 | }) 23 | }) 24 | } 25 | } 26 | }; 27 | 28 | Vue.use(ConfirmModal); 29 | -------------------------------------------------------------------------------- /modules/v1/workspaces/resources/UserWorkspaceResource.php: -------------------------------------------------------------------------------- 1 | hasOne(UserResource::class, ['id' => 'user_id']); 34 | } 35 | } -------------------------------------------------------------------------------- /vue/src/plugins/alert-dialog.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import i18n from "@/shared/i18n"; 3 | 4 | const ConfirmModal = { 5 | install(Vue) { 6 | Vue.prototype.$alert = function (message, title = i18n.t('Operation was not successful')) { 7 | return new Promise((resolve, reject) => { 8 | this.$bvModal.msgBoxOk(message, { 9 | title: title, 10 | okVariant: 'danger', 11 | headerBgVariant: 'danger', 12 | headerTextVariant: 'light', 13 | footerClass: 'p-2', 14 | hideHeaderClose: false, 15 | centered: true 16 | }) 17 | .then(value => { 18 | resolve(value) 19 | }) 20 | .catch(err => { 21 | reject(err); 22 | }) 23 | }) 24 | } 25 | } 26 | }; 27 | 28 | Vue.use(ConfirmModal); 29 | -------------------------------------------------------------------------------- /tests/bin/yii: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | [ 21 | 'db' => require __DIR__ . '/../../config/test_db.php' 22 | ] 23 | ] 24 | ); 25 | 26 | 27 | $application = new yii\console\Application($config); 28 | $exitCode = $application->run(); 29 | exit($exitCode); 30 | -------------------------------------------------------------------------------- /modules/v1/setup/models/query/CountryQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 15 | }*/ 16 | 17 | /** 18 | * {@inheritdoc} 19 | * @return \app\modules\v1\setup\models\Country[]|array 20 | */ 21 | public function all($db = null) 22 | { 23 | return parent::all($db); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | * @return \app\modules\v1\setup\models\Country|array|null 29 | */ 30 | public function one($db = null) 31 | { 32 | return parent::one($db); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vue/src/store/modules/setup/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_COUNTRIES_LOADING = 'setup/SET_COUNTRIES_LOADING'; 2 | export const SET_COUNTRIES = 'setup/SET_COUNTRIES'; 3 | export const SHOW_COUNTRY_MODAL = 'setup/SHOW_COUNTRY_MODAL'; 4 | export const HIDE_COUNTRY_MODAL = 'setup/HIDE_COUNTRY_MODAL'; 5 | export const UPDATE_COUNTRY = 'setup/UPDATE_COUNTRY'; 6 | export const CREATE_COUNTRY = 'setup/CREATE_COUNTRY'; 7 | export const SET_DEPARTMENTS = 'setup/SET_DEPARTMENTS'; 8 | export const SHOW_DEPARTMENT_MODAL = 'setup/SHOW_DEPARTMENT_MODAL'; 9 | export const HIDE_DEPARTMENT_MODAL = 'setup/HIDE_DEPARTMENT_MODAL'; 10 | export const SET_INVITATIONS_LOADING = 'setup/SET_INVITATIONS_LOADING'; 11 | export const SET_INVITATIONS = 'setup/SET_INVITATIONS'; 12 | export const SHOW_INVITATION_MODAL = 'setup/SHOW_INVITATION_MODAL'; 13 | export const HIDE_INVITATION_MODAL = 'setup/HIDE_INVITATION_MODAL'; 14 | -------------------------------------------------------------------------------- /vue/src/modules/setup/setupModule.js: -------------------------------------------------------------------------------- 1 | import MenuService from "../../core/components/sidebar/MenuService"; 2 | import MenuItem from "../../core/components/sidebar/MenuItem"; 3 | import i18n from './../../shared/i18n' 4 | 5 | MenuService.addItem(new MenuItem('setup', { 6 | text: i18n.t('Setup'), 7 | weight: 900, 8 | isGroup: true 9 | })); 10 | MenuService.addItem(new MenuItem('setup.countries', { 11 | text: i18n.t('Countries'), 12 | path: '/setup/countries', 13 | weight: 910, 14 | icon: 'fas fa-globe-americas', 15 | })); 16 | MenuService.addItem(new MenuItem('setup.invitations', { 17 | text: i18n.t('Invitations'), 18 | path: '/setup/invitations', 19 | weight: 920, 20 | icon: 'fas fa-envelope-open-text', 21 | })); 22 | MenuService.addItem(new MenuItem('setup.users', { 23 | text: i18n.t('Users'), 24 | path: '/setup/users', 25 | weight: 930, 26 | icon: 'fas fa-users', 27 | })); 28 | -------------------------------------------------------------------------------- /vue/src/modules/Orgchart/Orgchart.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | 29 | -------------------------------------------------------------------------------- /vue/src/core/components/sidebar/MenuItem.js: -------------------------------------------------------------------------------- 1 | export default class MenuItem { 2 | 3 | constructor(name, { 4 | text, 5 | path = '', 6 | weight = 0, 7 | icon = '', 8 | image = '', 9 | linkOptions = {}, 10 | children = [], 11 | isGroup = false, 12 | favourite = false, 13 | buttonText = '', 14 | onClick = () => {} 15 | }) { 16 | if (!name) { 17 | throw new Error("Please provide name in MenuItem constructor"); 18 | } 19 | this.path = path; 20 | this.isGroup = isGroup; 21 | this.text = text; 22 | this.weight = weight; 23 | this.icon = icon; 24 | this.image = image; 25 | this.linkOptions = linkOptions; 26 | this.children = children.map(child => new MenuItem(child.name || child.text, child)); 27 | this.favourite = favourite; 28 | this.name = name; 29 | this.buttonText = buttonText; 30 | this.onClick = onClick; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | bootstrap: _bootstrap.php 3 | paths: 4 | tests: tests 5 | log: tests/_output 6 | data: tests/_data 7 | helpers: tests/_support 8 | settings: 9 | memory_limit: 1024M 10 | colors: true 11 | modules: 12 | config: 13 | Yii2: 14 | configFile: 'config/test.php' 15 | 16 | # To enable code coverage: 17 | #coverage: 18 | # #c3_url: http://localhost:8080/index-test.php/ 19 | # enabled: true 20 | # #remote: true 21 | # #remote_config: '../codeception.yml' 22 | # whitelist: 23 | # include: 24 | # - models/* 25 | # - controllers/* 26 | # - commands/* 27 | # - mail/* 28 | # blacklist: 29 | # include: 30 | # - assets/* 31 | # - config/* 32 | # - runtime/* 33 | # - vendor/* 34 | # - views/* 35 | # - web/* 36 | # - tests/* 37 | -------------------------------------------------------------------------------- /modules/v1/setup/controllers/CountryController.php: -------------------------------------------------------------------------------- 1 | 19 | * @package app\modules\v1\setup\controllers 20 | */ 21 | class CountryController extends ActiveController 22 | { 23 | public $modelClass = CountryResource::class; 24 | 25 | /** 26 | * @return array|mixed 27 | * @author Saiat Kalbiev 28 | */ 29 | public function actionOrgChartData() 30 | { 31 | $orgchartHelper = new OrgchartHelper(); 32 | return $orgchartHelper->getCharData(\Yii::$app->request->get('country_id')); 33 | } 34 | } -------------------------------------------------------------------------------- /vue/src/core/components/DropdownButtons.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /commands/HelloController.php: -------------------------------------------------------------------------------- 1 | 19 | * @since 2.0 20 | */ 21 | class HelloController extends Controller 22 | { 23 | /** 24 | * This command echoes what you have entered as the message. 25 | * @param string $message the message to be echoed. 26 | * @return int Exit code 27 | */ 28 | public function actionIndex($message = 'hello world') 29 | { 30 | echo $message . "\n"; 31 | 32 | return ExitCode::OK; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/WorkspaceActivityQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 15 | }*/ 16 | 17 | /** 18 | * {@inheritdoc} 19 | * @return \app\modules\v1\workspaces\models\WorkspaceActivity[]|array 20 | */ 21 | public function all($db = null) 22 | { 23 | return parent::all($db); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | * @return \app\modules\v1\workspaces\models\WorkspaceActivity|array|null 29 | */ 30 | public function one($db = null) 31 | { 32 | return parent::one($db); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/navbar.scss: -------------------------------------------------------------------------------- 1 | //Author : @arboshiki 2 | 3 | @mixin navbar-variant($navbar-bg, $navbar-brand-color, $navbar-color){ 4 | background: $navbar-bg; 5 | .navbar-brand{ 6 | color: $navbar-brand-color; 7 | } 8 | .navbar-nav{ 9 | >li>a{ 10 | color: $navbar-color; 11 | &:focus, 12 | &:hover{ 13 | color: lighten($navbar-color, 7%); 14 | } 15 | } 16 | 17 | >.open{ 18 | >a{ 19 | &, 20 | &:focus, 21 | &:hover{ 22 | background-color: rgba(0, 0, 0, 0.1); 23 | } 24 | } 25 | } 26 | } 27 | @media screen and (max-width: $screen-xs-min){ 28 | .navbar-items{ 29 | border-bottom-color: darken($navbar-bg, 5%); 30 | } 31 | .navbar-items-2{ 32 | background-color: $navbar-bg; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /vue/src/plugins/axios/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {AppSettings} from "@/shared/AppSettings"; 3 | import auth from './../../core/services/authService'; 4 | import router from "../../shared/router"; 5 | import Vue from "vue"; 6 | 7 | let config = { 8 | baseURL: AppSettings.url() 9 | }; 10 | 11 | const axiosClient = axios.create(config); 12 | 13 | axiosClient.interceptors.request.use(config => { 14 | if (auth.loggedIn()) { 15 | config.headers.Authorization = `Bearer ${auth.getToken()}`; 16 | } 17 | return config; 18 | }); 19 | 20 | axiosClient.interceptors.response.use( 21 | response => { 22 | return response; 23 | }, 24 | error => { 25 | if (error && error.response && error.response.status === 401) { 26 | router.push('/login') 27 | } 28 | return Promise.reject(error); 29 | } 30 | ); 31 | 32 | export default axiosClient; 33 | 34 | Vue.use({ 35 | install(Vue) { 36 | Vue.prototype.$http = axiosClient; 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /vue/src/modules/setup/countries/departments/DepartmentListGroup.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | 26 | 34 | -------------------------------------------------------------------------------- /migrations/m201013_125848_add_action_column_to_timeline_posts_table.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%timeline_posts}}', 'article_id', $this->integer()->after('id')); 16 | $this->addColumn('{{%timeline_posts}}', 'attachment_ids', $this->text()->after('article_id')); 17 | $this->addColumn('{{%timeline_posts}}', 'action', $this->string(255)->after('attachment_ids')); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function safeDown() 24 | { 25 | $this->dropColumn('{{%timeline_posts}}', 'article_id'); 26 | $this->dropColumn('{{%timeline_posts}}', 'attachment_ids'); 27 | $this->dropColumn('{{%timeline_posts}}', 'action'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/m200904_114621_add_demo_users.php: -------------------------------------------------------------------------------- 1 | insert('{{%users}}', [ 16 | 'id' => 1, 17 | 'username' => 'admin', 18 | 'email' => 'admin@agoraintra.net', 19 | 'password_hash' => Yii::$app->security->generatePasswordHash('admin'), 20 | 'password_reset_token' => null, 21 | 'access_token' => null, 22 | 'status' => 1, 23 | 'created_at' => time(), 24 | 'created_by' => 1, 25 | 'updated_at' => time(), 26 | 'updated_by' => 1 27 | ]); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function safeDown() 34 | { 35 | $this->delete('{{%users}}', ['id' => 1]); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /vue/src/core/components/ContentSpinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | -------------------------------------------------------------------------------- /vue/src/modules/Auth/RegisterModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../shared/i18n'; 3 | 4 | export default class RegisterModel extends BaseModel { 5 | firstname = ''; 6 | lastname = ''; 7 | password = ''; 8 | password_repeat = ''; 9 | email = ''; 10 | token = ''; 11 | 12 | rules = { 13 | firstname: 'required', 14 | lastname: 'required', 15 | email: 'email', 16 | password: [ 17 | {rule: 'required'}, 18 | {rule: 'min', length: 6}, 19 | ], 20 | password_repeat: [ 21 | {rule: 'required'}, 22 | {rule: 'confirmed', target: 'password'}, 23 | ], 24 | }; 25 | 26 | attributeLabels = { 27 | firstname: i18n.t('First Name'), 28 | lastname: i18n.t('Last Name'), 29 | email: i18n.t('Email'), 30 | password: i18n.t('Password'), 31 | password_repeat: i18n.t('Password Repeat') 32 | }; 33 | 34 | constructor(data = {}) { 35 | super(); 36 | Object.assign(this, {...data}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/buttons.scss: -------------------------------------------------------------------------------- 1 | @mixin btn-3d-variant($bg){ 2 | $shadow : $btn-3d-box-shadow-common1, $btn-3d-box-shadow-prefix darken($bg, 7%), $btn-3d-box-shadow-common2; 3 | @include box-shadow($shadow); 4 | &, &:hover{ 5 | background-color: $bg; 6 | } 7 | &:active{ 8 | $shadow : $btn-3d-box-shadow-common1, $btn-3d-active-box-shadow-prefix darken($bg, 7%); 9 | @include box-shadow($shadow); 10 | } 11 | } 12 | 13 | @mixin btn-circle-size($size, $padding, $font-size){ 14 | @include square($size); 15 | padding: $padding; 16 | font-size: $font-size; 17 | } 18 | 19 | @mixin btn-outline-variant($bg){ 20 | border: 1px solid $bg; 21 | color: $bg; 22 | &:focus{ 23 | background-color: $bg; 24 | } 25 | } 26 | 27 | @mixin btn-label-size($padding-vertical, $padding-horizontal, $width){ 28 | padding-left: $width + $padding-horizontal; 29 | .btn-label{ 30 | padding: $padding-vertical $padding-horizontal; 31 | width: $width; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/v1/workspaces/controllers/WorkspaceActivityController.php: -------------------------------------------------------------------------------- 1 | request->get('workspaceId')) { 18 | $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider']; 19 | } 20 | return $actions; 21 | } 22 | 23 | public function prepareDataProvider() 24 | { 25 | return new ActiveDataProvider([ 26 | 'query' => $this->modelClass::find()->andWhere(['workspace_id' => \Yii::$app->request->get('workspaceId')])->orderBy([ 27 | 'created_at' => SORT_DESC 28 | ]) 29 | ]); 30 | } 31 | } -------------------------------------------------------------------------------- /rest/ControllerAuthTrait.php: -------------------------------------------------------------------------------- 1 | 19 | * @package app\rest 20 | */ 21 | trait ControllerAuthTrait 22 | { 23 | use ControllerTrait; 24 | 25 | public function behaviors() 26 | { 27 | $behaviors = parent::behaviors(); 28 | 29 | $auth = $behaviors['authenticator']; 30 | unset($behaviors['authenticator']); 31 | $behaviors['cors'] = [ 32 | 'class' => Cors::class 33 | ]; 34 | $auth['except'] = ['options']; 35 | $auth['authMethods'] = [ 36 | HttpBearerAuth::class, 37 | QueryParamAuth::class, 38 | ]; 39 | 40 | $behaviors['authenticator'] = $auth; 41 | 42 | return $behaviors; 43 | } 44 | } -------------------------------------------------------------------------------- /docker/vhost.conf: -------------------------------------------------------------------------------- 1 | ## API ## 2 | server { 3 | listen 80; 4 | 5 | root /app/web; 6 | index index.php index.html; 7 | 8 | server_name agora-api.localhost; 9 | 10 | charset utf-8; 11 | 12 | location / { 13 | try_files $uri $uri/ /index.php?$args; 14 | } 15 | 16 | client_max_body_size 32m; 17 | 18 | # There is a VirtualBox bug related to sendfile that can lead to 19 | # corrupted files, if not turned-off 20 | # sendfile off; 21 | 22 | location ~ \.php$ { 23 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 24 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 25 | fastcgi_pass php-fpm; 26 | fastcgi_index index.php; 27 | include fastcgi_params; 28 | } 29 | } 30 | 31 | 32 | ## PHP-FPM Servers ## 33 | upstream php-fpm { 34 | server app:9000; 35 | } 36 | 37 | ## MISC ## 38 | ### WWW Redirect ### 39 | server { 40 | listen 80; 41 | server_name www.agora-api.localhost; 42 | return 301 http://agora-api.localhost$request_uri; 43 | } 44 | -------------------------------------------------------------------------------- /vue/src/plugins/toaster.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import BootstrapVue from './bootstrap-vue'; 3 | import i18n from "../shared/i18n"; 4 | 5 | const vueBootstrap = new (Vue.extend(BootstrapVue)); 6 | 7 | function getVariant(variant) { 8 | const availableVariants = ['secondary', 'primary', 'danger', 'warning', 'success', 'info', 'default']; 9 | return availableVariants.includes(variant) ? variant : 'default'; 10 | } 11 | 12 | function getTitle(variant) { 13 | if (variant === 'success') { 14 | return i18n.t('Success'); 15 | } else if (variant === 'danger') { 16 | return i18n.t('Error') 17 | } else { 18 | return i18n.t('Info') 19 | } 20 | } 21 | 22 | const Toaster = { 23 | install(Vue) { 24 | Vue.prototype.$toast = (message, variant = 'success') => { 25 | vueBootstrap.$bvToast.toast(message, { 26 | title: getTitle(variant), 27 | autoHideDelay: 5000, 28 | appendToast: false, 29 | variant: getVariant(variant) 30 | }); 31 | } 32 | } 33 | }; 34 | 35 | Vue.use(Toaster); 36 | 37 | export default Toaster; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | args: 6 | user: agora 7 | uid: 1000 8 | context: ./ 9 | dockerfile: docker/php/Dockerfile 10 | networks: 11 | - agora 12 | volumes: 13 | - ./:/app 14 | depends_on: 15 | - db 16 | env_file: 17 | - .env 18 | 19 | nginx: 20 | image: nginx:1.12-alpine 21 | networks: 22 | - agora 23 | ports: 24 | - 80:80 25 | volumes: 26 | - ./:/app 27 | - ./docker/vhost.conf:/etc/nginx/conf.d/vhost.conf 28 | depends_on: 29 | - app 30 | 31 | db: 32 | image: mysql:5.7 33 | networks: 34 | - agora 35 | volumes: 36 | - /var/lib/mysql 37 | - ./docker/mysql/config.cnf:/etc/mysql/conf.d/config.cnf 38 | ports: 39 | - 3307:3306 40 | environment: 41 | MYSQL_ROOT_PASSWORD: root 42 | MYSQL_DATABASE: yii2_agora 43 | MYSQL_USER: yii2_agora 44 | MYSQL_PASSWORD: 123456 45 | 46 | networks: 47 | agora: 48 | external: 49 | name: nat -------------------------------------------------------------------------------- /vue/src/modules/setup/employees/UserDepartmentModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from "../../../shared/i18n"; 3 | 4 | export default class EmployeeModel extends BaseModel { 5 | id = null; 6 | position = null; 7 | country_id = null; 8 | department_id = null; 9 | 10 | rules = { 11 | id: [ 12 | {rule: 'required'} 13 | ], 14 | position: [ 15 | {rule: 'required'} 16 | ], 17 | country_id: [ 18 | {rule: 'required'} 19 | ], 20 | department_id: [ 21 | {rule: 'required'} 22 | ], 23 | } 24 | 25 | attributeLabels = { 26 | id: ' ', 27 | position: 'Position', 28 | country_id: 'Country', 29 | department_id: 'Department', 30 | }; 31 | 32 | toJSON() { 33 | let data = super.toJSON(); 34 | //TODO: discuss how to remove extra field without creating bug on initialisation in EmployeeModel.js 35 | // delete data.country_id; 36 | return data; 37 | } 38 | 39 | constructor(data) { 40 | super(); 41 | Object.assign(this, {...data}) 42 | } 43 | } -------------------------------------------------------------------------------- /modules/v1/workspaces/workspaceBehaviours/UrlAnchorBehaviour.php: -------------------------------------------------------------------------------- 1 | attributes)) { 17 | return [ 18 | ActiveRecord::EVENT_BEFORE_INSERT => 'beforeInsert', 19 | ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeInsert', 20 | ]; 21 | } 22 | return []; 23 | } 24 | 25 | public function beforeInsert() 26 | { 27 | // Find all links which is not 28 | foreach ($this->attributes as $attribute) { 29 | $this->owner->{$attribute} = preg_replace("/(?$2', $this->owner->{$attribute}); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /vue/src/modules/Workspace/components/comment/DeleteComment.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | 32 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/TimelinePostQuery.php: -------------------------------------------------------------------------------- 1 | 38 | */ 39 | public function byWorkspaceId($id) 40 | { 41 | list($t, $a) = $this->getTableNameAndAlias(); 42 | return $this->andWhere([$a . '.workspace_id' => $id]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vue/src/core/components/LikeUnlikeButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | -------------------------------------------------------------------------------- /modules/v1/setup/models/query/UserDepartmentQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 15 | }*/ 16 | 17 | /** 18 | * {@inheritdoc} 19 | * @return \app\modules\v1\setup\models\UserDepartment[]|array 20 | */ 21 | public function all($db = null) 22 | { 23 | return parent::all($db); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | * @return \app\modules\v1\setup\models\UserDepartment|array|null 29 | */ 30 | public function one($db = null) 31 | { 32 | return parent::one($db); 33 | } 34 | 35 | /** 36 | * Find departments by user_id 37 | * 38 | * @param $user_id 39 | * @return UserDepartmentQuery 40 | */ 41 | public function byUserId($user_id) { 42 | return $this->andWhere(['user_id' => $user_id]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/acceptance/ContactCest.php: -------------------------------------------------------------------------------- 1 | amOnPage(Url::toRoute('/site/contact')); 10 | } 11 | 12 | public function contactPageWorks(AcceptanceTester $I) 13 | { 14 | $I->wantTo('ensure that contact page works'); 15 | $I->see('Contact', 'h1'); 16 | } 17 | 18 | public function contactFormCanBeSubmitted(AcceptanceTester $I) 19 | { 20 | $I->amGoingTo('submit contact form with correct data'); 21 | $I->fillField('#contactform-name', 'tester'); 22 | $I->fillField('#contactform-email', 'tester@example.com'); 23 | $I->fillField('#contactform-subject', 'test subject'); 24 | $I->fillField('#contactform-body', 'test content'); 25 | $I->fillField('#contactform-verifycode', 'testme'); 26 | 27 | $I->click('contact-button'); 28 | 29 | $I->wait(2); // wait for button to be clicked 30 | 31 | $I->dontSeeElement('#contact-form'); 32 | $I->see('Thank you for contacting us. We will respond to you as soon as possible.'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rest/ControllerTrait.php: -------------------------------------------------------------------------------- 1 | Cors::class, 28 | ]; 29 | 30 | return $behaviors; 31 | } 32 | 33 | /** 34 | * @param $message 35 | * @param $statusCode 36 | * @return array 37 | */ 38 | public function validationError($message, $statusCode = 422) 39 | { 40 | return $this->response($message, $statusCode); 41 | } 42 | 43 | /** 44 | * @param $data 45 | * @param int $statusCode 46 | * @return mixed 47 | * @author Zura Sekhniashvili 48 | */ 49 | public function response($data, $statusCode = 200) 50 | { 51 | Yii::$app->response->statusCode = $statusCode; 52 | return $data; 53 | } 54 | } -------------------------------------------------------------------------------- /vagrant/nginx/app.conf: -------------------------------------------------------------------------------- 1 | server { 2 | charset utf-8; 3 | client_max_body_size 128M; 4 | sendfile off; 5 | 6 | listen 80; ## listen for ipv4 7 | #listen [::]:80 default_server ipv6only=on; ## listen for ipv6 8 | 9 | server_name yii2basic.test; 10 | root /app/web/; 11 | index index.php; 12 | 13 | access_log /app/vagrant/nginx/log/yii2basic.access.log; 14 | error_log /app/vagrant/nginx/log/yii2basic.error.log; 15 | 16 | location / { 17 | # Redirect everything that isn't a real file to index.php 18 | try_files $uri $uri/ /index.php$is_args$args; 19 | } 20 | 21 | # uncomment to avoid processing of calls to non-existing static files by Yii 22 | #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ { 23 | # try_files $uri =404; 24 | #} 25 | #error_page 404 /404.html; 26 | 27 | location ~ \.php$ { 28 | include fastcgi_params; 29 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 30 | #fastcgi_pass 127.0.0.1:9000; 31 | fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; 32 | try_files $uri =404; 33 | } 34 | 35 | location ~ /\.(ht|svn|git) { 36 | deny all; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/v1/setup/controllers/DepartmentController.php: -------------------------------------------------------------------------------- 1 | 20 | * @package app\modules\v1\setup\controllers 21 | */ 22 | class DepartmentController extends ActiveController 23 | { 24 | public $modelClass = DepartmentResource::class; 25 | 26 | /** 27 | * @return array 28 | * @author Saiat Kalbiev 29 | */ 30 | public function actions() 31 | { 32 | $actions = parent::actions(); 33 | unset($actions['index']); 34 | return $actions; 35 | } 36 | 37 | /** 38 | * @return \yii\data\ActiveDataProvider 39 | * @author Saiat Kalbiev 40 | */ 41 | public function actionIndex() 42 | { 43 | return (new DepartmentSearch())->search(Yii::$app->request->get()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/v1/setup/models/query/DepartmentQuery.php: -------------------------------------------------------------------------------- 1 | andWhere('[[status]]=1'); 17 | }*/ 18 | 19 | /** 20 | * {@inheritdoc} 21 | * @return \app\modules\v1\setup\models\Department[]|array 22 | */ 23 | public function all($db = null) 24 | { 25 | return parent::all($db); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | * @return \app\modules\v1\setup\models\Department|array|null 31 | */ 32 | public function one($db = null) 33 | { 34 | return parent::one($db); 35 | } 36 | 37 | /** 38 | * @param $id 39 | * @return DepartmentQuery 40 | * @author Saiat Kalbiev 41 | */ 42 | public function byCountryId($id) 43 | { 44 | return $this->andWhere([Department::tableName() . '.country_id' => $id]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vue/src/core/scss/main.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | display: flex; 3 | align-items: center; 4 | padding: 1rem; 5 | background-color: #FFF; 6 | box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.1); 7 | 8 | //@media (max-width: 576px){ 9 | // padding: 0; 10 | //} 11 | 12 | .nav-pills { 13 | box-shadow: none; 14 | flex-wrap: nowrap; 15 | } 16 | 17 | .nav-link { 18 | padding-top: 1.1rem; 19 | padding-bottom: 1.1rem; 20 | } 21 | 22 | @media (max-width: 576px) { 23 | .nav-link { 24 | padding-left: 0.5rem; 25 | padding-right: 0.5rem; 26 | } 27 | } 28 | 29 | h1 { 30 | font-size: 1.5rem; 31 | margin-bottom: 0; 32 | flex: 1; 33 | white-space: nowrap; 34 | } 35 | 36 | .breadcrumb { 37 | flex: 1; 38 | background: transparent; 39 | margin: 0; 40 | } 41 | 42 | .page-header-fixed.menu-fixed & { 43 | left: $menu-width; 44 | } 45 | 46 | @include media-breakpoint-down(sm) { 47 | .page-header-fixed.menu-fixed & { 48 | left: 0; 49 | } 50 | 51 | display: flex; 52 | justify-content: space-between; 53 | align-items: center; 54 | flex-direction: row; 55 | 56 | } 57 | 58 | .page-header-fixed.menu-fixed.menu-hidden & { 59 | left: 0; 60 | } 61 | } -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/ArticleQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([Article::tableName() . '.id' => $id]); 42 | } 43 | 44 | /** 45 | * @param $workspaceId 46 | * @return ArticleQuery 47 | */ 48 | public function byWorkspaceId($workspaceId) 49 | { 50 | return $this->andWhere([Article::tableName() . '.workspace_id' => $workspaceId]); 51 | } 52 | } -------------------------------------------------------------------------------- /config/test.php: -------------------------------------------------------------------------------- 1 | 'basic-tests', 10 | 'basePath' => dirname(__DIR__), 11 | 'aliases' => [ 12 | '@bower' => '@vendor/bower-asset', 13 | '@npm' => '@vendor/npm-asset', 14 | ], 15 | 'language' => 'en-US', 16 | 'components' => [ 17 | 'db' => $db, 18 | 'mailer' => [ 19 | 'useFileTransport' => true, 20 | ], 21 | 'assetManager' => [ 22 | 'basePath' => __DIR__ . '/../web/assets', 23 | ], 24 | 'urlManager' => [ 25 | 'showScriptName' => true, 26 | ], 27 | 'user' => [ 28 | 'identityClass' => 'app\models\User', 29 | ], 30 | 'request' => [ 31 | 'cookieValidationKey' => 'test', 32 | 'enableCsrfValidation' => false, 33 | // but if you absolutely need it set cookie domain to localhost 34 | /* 35 | 'csrfCookie' => [ 36 | 'domain' => 'localhost', 37 | ], 38 | */ 39 | ], 40 | ], 41 | 'params' => $params, 42 | ]; 43 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/components/comment/ChildCommentItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /vue/src/core/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 12 | 23 | 54 | -------------------------------------------------------------------------------- /helpers/ModelHelper.php: -------------------------------------------------------------------------------- 1 | function () { 19 | return Yii::$app->formatter->asDatetime($this->created_at); 20 | }, 21 | 'updated_at' => function () { 22 | return Yii::$app->formatter->asDatetime($this->created_at); 23 | }, 24 | 25 | ]; 26 | } 27 | 28 | public function extraFields() 29 | { 30 | return ['department', 'user']; 31 | } 32 | 33 | public function getDepartment() 34 | { 35 | return $this->hasOne(DepartmentResource::class, ['id' => 'department_id']); 36 | } 37 | 38 | /** 39 | * @return \app\modules\v1\setup\models\query\UserQuery|\yii\db\ActiveQuery 40 | * @author Saiat Kalbiev 41 | */ 42 | public function getUser() 43 | { 44 | return $this->hasOne(UserResource::class, ['id' => 'user_id']); 45 | } 46 | } -------------------------------------------------------------------------------- /vue/src/core/components/sided-nav-layout/SidedNavLayout.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | -------------------------------------------------------------------------------- /migrations/m201109_113741_alter_table_timeline_posts_add_column_workspace_id.php: -------------------------------------------------------------------------------- 1 | addColumn('{{%timeline_posts}}', 'workspace_id', $this->integer(11)->after('id')); 16 | $this->addForeignKey( 17 | 'fk_timeline_posts_workspace', 18 | '{{%timeline_posts}}', 19 | 'workspace_id', 20 | '{{%workspaces}}', 21 | "id", 22 | "CASCADE" 23 | ); 24 | $this->createIndex( 25 | '{{%idx-timeline_posts_workspace_id}}', 26 | '{{%timeline_posts}}', 27 | 'workspace_id' 28 | ); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function safeDown() 35 | { 36 | $this->dropForeignKey('fk_timeline_posts_workspace', '{{%timeline_posts}}'); 37 | $this->dropIndex('{{%idx-timeline_posts_workspace_id}}', '{{%timeline_posts}}'); 38 | $this->dropColumn('{{%timeline_posts}}', 'workspace_id'); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /tests/unit/models/UserTest.php: -------------------------------------------------------------------------------- 1 | username)->equals('admin'); 13 | 14 | expect_not(User::findIdentity(999)); 15 | } 16 | 17 | public function testFindUserByAccessToken() 18 | { 19 | expect_that($user = User::findIdentityByAccessToken('100-token')); 20 | expect($user->username)->equals('admin'); 21 | 22 | expect_not(User::findIdentityByAccessToken('non-existing')); 23 | } 24 | 25 | public function testFindUserByUsername() 26 | { 27 | expect_that($user = User::findByUsername('admin')); 28 | expect_not(User::findByUsername('not-admin')); 29 | } 30 | 31 | /** 32 | * @depends testFindUserByUsername 33 | */ 34 | public function testValidateUser($user) 35 | { 36 | $user = User::findByUsername('admin'); 37 | expect_that($user->validateAuthKey('test100key')); 38 | expect_not($user->validateAuthKey('test102key')); 39 | 40 | expect_that($user->validatePassword('admin')); 41 | expect_not($user->validatePassword('123456')); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /vue/src/core/components/navbar/components/SidebarToggle.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | 27 | 53 | -------------------------------------------------------------------------------- /vue/src/modules/Orgchart/OrgchartBody.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 45 | 46 | -------------------------------------------------------------------------------- /vue/src/core/components/SubmitButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 52 | 53 | 56 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/WorkspaceQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([Workspace::tableName() . '.id' => $id]); 43 | } 44 | 45 | /** 46 | * @param $userId 47 | * @return WorkspaceQuery 48 | */ 49 | public function byUserId($userId) 50 | { 51 | return $this->innerJoin(UserWorkspace::tableName() . ' uw', 'uw.workspace_id = ' . Workspace::tableName() . '.id') 52 | ->andWhere(['uw.user_id' => $userId]); 53 | } 54 | } -------------------------------------------------------------------------------- /vue/src/store/modules/workspaces/state.js: -------------------------------------------------------------------------------- 1 | /** @var { WorkspaceState } */ 2 | const STATE = { 3 | showModal: false, 4 | workspaces: [], 5 | loading: false, 6 | modalWorkspace: null, 7 | 8 | view: { 9 | loading: false, 10 | workspace: {}, 11 | articles: { 12 | loading: false, 13 | data: [], 14 | modal: { 15 | show: false, 16 | object: null 17 | }, 18 | view: { 19 | article: null, 20 | loading: false, 21 | }, 22 | }, 23 | timeline: { 24 | loading: false, 25 | data: [], 26 | modal: { 27 | loading: false, 28 | show: false, 29 | object: null, 30 | } 31 | }, 32 | folders: { 33 | loading: false, 34 | folderAndFiles: [], 35 | breadcrumb: [], 36 | folder: {}, 37 | data: [], 38 | modal: { 39 | show: false, 40 | object: null, 41 | isFile: false, 42 | }, 43 | previewModal: { 44 | show: false, 45 | files: [], 46 | activeFile: null, 47 | }, 48 | attachConfig: {}, 49 | }, 50 | activity: { 51 | loading: false, 52 | data: [] 53 | }, 54 | users: { 55 | loading: false, 56 | data: [], 57 | }, 58 | inviteModal: { 59 | show: false, 60 | users: [], 61 | }, 62 | } 63 | }; 64 | 65 | export default STATE; 66 | -------------------------------------------------------------------------------- /migrations/m201119_094300_create_user_activity_table.php: -------------------------------------------------------------------------------- 1 | createTable('{{%user_activity}}', [ 16 | 'id' => $this->primaryKey(), 17 | 'workspace_id' => $this->integer(), 18 | 'table_name' => $this->string(128), 19 | 'content_id' => $this->integer(), 20 | 'action' => $this->string(128), 21 | 'description' => 'LONGTEXT', 22 | 'created_at' => $this->integer(), 23 | 'created_by' => $this->integer(), 24 | ]); 25 | $this->addForeignKey('FK_user_activity_users_created_by', '{{%user_activity}}', 'created_by', '{{%users}}', 'id'); 26 | $this->addForeignKey('FK_user_activity_workspaces_workspaces_id', '{{%user_activity}}', 'workspace_id', '{{%workspaces}}', 'id'); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function safeDown() 33 | { 34 | $this->dropForeignKey('FK_user_activity_users_created_by', '{{%user_activity}}'); 35 | $this->dropForeignKey('FK_user_activity_workspaces_workspaces_id', '{{%user_activity}}'); 36 | $this->dropTable('{{%user_activity}}'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/models/LoginFormTest.php: -------------------------------------------------------------------------------- 1 | user->logout(); 15 | } 16 | 17 | public function testLoginNoUser() 18 | { 19 | $this->model = new LoginForm([ 20 | 'username' => 'not_existing_username', 21 | 'password' => 'not_existing_password', 22 | ]); 23 | 24 | expect_not($this->model->login()); 25 | expect_that(\Yii::$app->user->isGuest); 26 | } 27 | 28 | public function testLoginWrongPassword() 29 | { 30 | $this->model = new LoginForm([ 31 | 'username' => 'demo', 32 | 'password' => 'wrong_password', 33 | ]); 34 | 35 | expect_not($this->model->login()); 36 | expect_that(\Yii::$app->user->isGuest); 37 | expect($this->model->errors)->hasKey('password'); 38 | } 39 | 40 | public function testLoginCorrect() 41 | { 42 | $this->model = new LoginForm([ 43 | 'username' => 'demo', 44 | 'password' => 'demo', 45 | ]); 46 | 47 | expect_that($this->model->login()); 48 | expect_not(\Yii::$app->user->isGuest); 49 | expect($this->model->errors)->hasntKey('password'); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /vue/src/modules/User/UserModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../core/components/input-widget/BaseModel"; 2 | import i18n from '../../shared/i18n'; 3 | 4 | export default class UserModel extends BaseModel { 5 | email = ''; 6 | old_password = ''; 7 | password = ''; 8 | confirm_password = ''; 9 | first_name = ''; 10 | last_name = ''; 11 | phone = ''; 12 | mobile = ''; 13 | birthday = ''; 14 | about_me = ''; 15 | image = null; 16 | hobbies = []; 17 | 18 | rules = { 19 | email: [ 20 | {rule: 'email'}, 21 | {rule: 'required'}, 22 | ], 23 | password: '', 24 | confirm_password: [ 25 | {rule: 'confirmed', target: 'password'}, 26 | ], 27 | first_name: '', 28 | last_name: '', 29 | phone: '', 30 | mobile: '', 31 | birthday: '', 32 | about_me: '', 33 | hobbies: '', 34 | image: '' 35 | }; 36 | 37 | attributeLabels = { 38 | email: i18n.t('Email'), 39 | old_password: i18n.t('Old Password'), 40 | password: i18n.t('Password'), 41 | confirm_password: i18n.t('Confirm Password'), 42 | first_name: i18n.t('First Name'), 43 | last_name: i18n.t('Last Name'), 44 | birthday: i18n.t('Birthday'), 45 | phone: i18n.t('Phone'), 46 | mobile: i18n.t('Mobile'), 47 | about_me: i18n.t('About Me'), 48 | hobbies: i18n.t('Hobbies'), 49 | }; 50 | 51 | constructor(data = {}) { 52 | super(); 53 | Object.assign(this, {...data}); 54 | } 55 | } -------------------------------------------------------------------------------- /vue/src/core/scss/mixins.scss: -------------------------------------------------------------------------------- 1 | @import "mixins/outline.scss"; 2 | @import "mixins/buttons.scss"; 3 | @import "mixins/panels.scss"; 4 | @import "mixins/switches.scss"; 5 | @import "mixins/checkboxes.scss"; 6 | @import "mixins/radios.scss"; 7 | @import "mixins/dropdowns.scss"; 8 | @import "mixins/callouts.scss"; 9 | @import "mixins/popovers.scss"; 10 | @import "mixins/tooltips.scss"; 11 | @import "mixins/tables.scss"; 12 | @import "mixins/forms.scss"; 13 | @import "mixins/navbar.scss"; 14 | @import "mixins/menu.scss"; 15 | @import "mixins/size.scss"; 16 | @import "mixins/vendor-prefixes.scss"; 17 | 18 | 19 | //------------------------------------------------------------------------------ 20 | 21 | @mixin nav-menu-submenu($current-level, $padding: $nav-menu-submenu-item-padding-horizontal){ 22 | @if $current-level <= $nav-menu-submenu-levels { 23 | >ul{ 24 | >li{ 25 | >a{ 26 | padding-left: $nav-menu-submenu-vertical-line-left-offset + $nav-menu-submenu-item-left-line-width + $nav-menu-submenu-padding-left-base - $nav-menu-submenu-item-padding-horizontal + $padding * ($current-level - 1); 27 | } 28 | @include nav-menu-submenu($current-level+1, $padding); 29 | } 30 | } 31 | } 32 | } 33 | @mixin rotate($rotateDegree){ 34 | transform: rotate($rotateDegree); 35 | -moz-transform: rotate($rotateDegree); 36 | -ms-transform: rotate($rotateDegree); 37 | } 38 | -------------------------------------------------------------------------------- /vue/src/store/modules/employee/mutations.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DATA, 3 | SET_MODAL_DROPDOWN_DATA, 4 | SHOW_EMPLOYEE_MODAL, 5 | CHANGE_LOADING, 6 | HIDE_MODAL, 7 | DELETED_USER, 8 | CHANGE_USER_ROLE, REMOVE_USER_FROM_WORKSPACE 9 | } from "./mutation-types"; 10 | 11 | export default { 12 | [SET_DATA]: (state, {rows}) => { 13 | state.data.rows = rows; 14 | }, 15 | 16 | [SET_MODAL_DROPDOWN_DATA]: (state, {userRoles, userPositions, countries, departments}) => { 17 | state.modalDropdownData.userRoles = userRoles; 18 | state.modalDropdownData.userPositions = userPositions; 19 | state.modalDropdownData.countries = countries; 20 | state.modalDropdownData.departments = departments; 21 | }, 22 | 23 | [SHOW_EMPLOYEE_MODAL]: (state, payload) => { 24 | state.modal.show = true; 25 | state.modal.object = { 26 | ...payload 27 | }; 28 | }, 29 | 30 | [CHANGE_LOADING]: (state) => state.loading = !state.loading, 31 | 32 | [HIDE_MODAL]: (state) => { 33 | state.modal.show = false; 34 | state.modal.object = {}; 35 | }, 36 | 37 | [DELETED_USER](state, id) { 38 | state.data.rows = state.data.rows.filter(a => a.id !== id) 39 | }, 40 | 41 | [CHANGE_USER_ROLE](state, {userId, role}) { 42 | const user = state.data.rows.find(u => u.id === userId); 43 | if (user) user.role = role; 44 | }, 45 | [REMOVE_USER_FROM_WORKSPACE](state, {userId}) { 46 | state.data.rows = state.data.rows.filter(u => u.id !== userId); 47 | } 48 | } -------------------------------------------------------------------------------- /config/common.php: -------------------------------------------------------------------------------- 1 | 'Agora Social Intranet', 8 | 'aliases' => [ 9 | '@bower' => '@vendor/bower-asset', 10 | '@npm' => '@vendor/npm-asset', 11 | '@portalUrl' => env('PORTAL_HOST'), 12 | '@storage' => dirname(__DIR__) . '/web/storage', 13 | '@storageUrl' => env('API_HOST') . '/storage', 14 | ], 15 | 'components' => [ 16 | 'cache' => [ 17 | 'class' => 'yii\caching\FileCache', 18 | ], 19 | 'mailer' => [ 20 | 'class' => \intermundia\mailer\SwiftMailer::class, 21 | 'useFileTransport' => env('MAILER_FILE_TRANSPORT'), 22 | 'transport' => [ 23 | 'class' => 'Swift_SmtpTransport', 24 | 'host' => env('SMTP_HOST'), 25 | 'username' => env('SMTP_USERNAME'), 26 | 'password' => env('SMTP_PASSWORD'), 27 | 'port' => env('SMTP_PORT'), 28 | 'encryption' => env('SMTP_ENCRYPTION'), 29 | ] 30 | ], 31 | 'log' => [ 32 | 'traceLevel' => YII_DEBUG ? 3 : 0, 33 | 'targets' => [ 34 | [ 35 | 'class' => 'yii\log\FileTarget', 36 | 'levels' => ['error', 'warning'], 37 | ], 38 | ], 39 | ], 40 | 'db' => $db, 41 | ], 42 | 'params' => $params, 43 | ]; -------------------------------------------------------------------------------- /modules/v1/setup/models/search/DepartmentSearch.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DepartmentSearch extends DepartmentResource 16 | { 17 | /** 18 | * @return array|array[] 19 | * @author Saiat Kalbiev 20 | */ 21 | public function rules() 22 | { 23 | return [ 24 | [['id', 'country_id'], 'integer'], 25 | [['name'], 'string'], 26 | ]; 27 | } 28 | 29 | /** 30 | * @param $params 31 | * @return ActiveDataProvider 32 | * @author Saiat Kalbiev 33 | */ 34 | public function search($params) 35 | { 36 | $query = self::find(); 37 | 38 | $dataProvider = new ActiveDataProvider(['query' => $query,]); 39 | 40 | if (!$this->load($params, '') || !$this->validate()) { 41 | return $dataProvider; 42 | } 43 | 44 | $query->andFilterWhere([ 45 | self::tableName() . '.id' => $this->id, 46 | self::tableName() . '.country_id' => $this->country_id, 47 | ]); 48 | 49 | $query->andFilterWhere(['LIKE', self::tableName() . '.name', $this->name]); 50 | 51 | return $dataProvider; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /migrations/m201127_100743_create_workspace_activity_table.php: -------------------------------------------------------------------------------- 1 | createTable('{{%workspace_activity}}', [ 16 | 'id' => $this->primaryKey(), 17 | 'workspace_id' => $this->integer(), 18 | 'table_name' => $this->string(128), 19 | 'content_id' => $this->integer(), 20 | 'action' => $this->string(128), 21 | 'description' => 'LONGTEXT', 22 | 'data' => $this->text(), 23 | 'created_at' => $this->integer(), 24 | 'created_by' => $this->integer(), 25 | ]); 26 | $this->addForeignKey('FK_workspace_activity_users_created_by', '{{%workspace_activity}}', 'created_by', '{{%users}}', 'id'); 27 | $this->addForeignKey('FK_workspace_activity_workspaces_workspaces_id', '{{%workspace_activity}}', 'workspace_id', '{{%workspaces}}', 'id'); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function safeDown() 34 | { 35 | $this->dropForeignKey('FK_workspace_activity_users_created_by', '{{%workspace_activity}}'); 36 | $this->dropForeignKey('FK_workspace_activity_workspaces_workspaces_id', '{{%workspace_activity}}'); 37 | $this->dropTable('{{%workspace_activity}}'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/view/articles/ArticleView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 46 | 47 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/UserLikeQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([UserLike::tableName() . '.id' => $id]); 43 | } 44 | 45 | /** 46 | * @param $articleId 47 | * @return UserLikeQuery 48 | */ 49 | public function byArticleId($articleId) 50 | { 51 | return $this->andWhere([UserLike::tableName() . '.article_id' => $articleId]); 52 | } 53 | 54 | /** 55 | * @param $timelinePostId 56 | * @return UserLikeQuery 57 | */ 58 | public function byTimelinePostId($timelinePostId) 59 | { 60 | return $this->andWhere([UserLike::tableName() . '.timeline_post_id' => $timelinePostId]); 61 | } 62 | } -------------------------------------------------------------------------------- /modules/v1/setup/models/query/InvitationQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([Invitation::tableName() . '.use_date' => null]); 41 | } 42 | 43 | /** 44 | * Find invitations by token 45 | * 46 | * @param $token 47 | * @return mixed 48 | */ 49 | public function byToken($token) 50 | { 51 | return $this->andWhere([Invitation::tableName() . '.token' => $token]); 52 | } 53 | 54 | /** 55 | * Find users by email 56 | * 57 | * @param $email 58 | * @return mixed 59 | */ 60 | public function byEmail($email) 61 | { 62 | return $this->andWhere([Invitation::tableName() . '.email' => $email]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software LLC nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-fpm 2 | 3 | ARG user 4 | ARG uid 5 | 6 | # Install system dependencies 7 | RUN apt-get update 8 | RUN apt-get install -y --no-install-recommends \ 9 | git \ 10 | curl \ 11 | zip \ 12 | unzip \ 13 | wget 14 | 15 | RUN apt-get install -y --no-install-recommends \ 16 | # needed for gd 17 | libfreetype6-dev \ 18 | libjpeg62-turbo-dev \ 19 | libpng-dev \ 20 | libonig-dev \ 21 | libxml2-dev \ 22 | && docker-php-ext-configure \ 23 | gd --with-jpeg 24 | 25 | # Clear cache 26 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 27 | 28 | # Install PHP extensions 29 | RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd intl 30 | 31 | RUN pecl install -o -f xdebug-2.9.8 \ 32 | && rm -rf /tmp/pear 33 | 34 | # Get latest Composer 35 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 36 | 37 | COPY ./docker/php/php.ini /usr/local/etc/php/ 38 | #COPY ./www.conf /usr/local/etc/php/ 39 | 40 | # Create system user to run Composer and Artisan Commands 41 | RUN useradd -G www-data,root -u $uid -d /home/$user $user 42 | RUN mkdir -p /home/$user/.composer && \ 43 | chown -R $user:$user /home/$user 44 | 45 | WORKDIR /app 46 | 47 | # Download webp image optimization tool and make it available using command "cwebp" 48 | RUN wget https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.1.0-linux-x86-64.tar.gz 49 | RUN tar -xf libwebp-1.1.0-linux-x86-64.tar.gz 50 | RUN cp libwebp-1.1.0-linux-x86-64/bin/cwebp /usr/local/bin/ 51 | RUN rm -rf libwebp-1.1.0-linux-x86-6* 52 | 53 | USER $user 54 | EXPOSE 9000 55 | CMD ["php-fpm"] 56 | -------------------------------------------------------------------------------- /modules/v1/setup/resources/CountryResource.php: -------------------------------------------------------------------------------- 1 | 19 | * @package app\modules\v1\setup\resources 20 | */ 21 | class CountryResource extends Country 22 | { 23 | public function fields() 24 | { 25 | return [ 26 | 'id', 'name', 'created_at' => function () { 27 | return \Yii::$app->formatter->asDatetime($this->created_at); 28 | } 29 | ]; 30 | } 31 | 32 | public function extraFields() 33 | { 34 | return ['createdBy', 'departments']; 35 | } 36 | 37 | public function getCreatedBy() 38 | { 39 | return $this->hasOne(UserResource::class, ['id' => 'created_by']); 40 | } 41 | 42 | public function beforeDelete() 43 | { 44 | if ($this->getDepartments()->count() > 0) { 45 | throw new ValidationException(\Yii::t('app', "You can't delete the country because it has departments")); 46 | } 47 | return parent::beforeDelete(); 48 | } 49 | 50 | /** 51 | * @return \app\modules\v1\setup\models\query\DepartmentQuery|\yii\db\ActiveQuery 52 | * @author Zura Sekhniashvili 53 | */ 54 | public function getDepartments() 55 | { 56 | return $this->hasMany(DepartmentResource::class, ['country_id' => 'id']); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/UserWorkspaceQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([UserWorkspace::tableName() . '.user_id' => $userId]); 44 | } 45 | 46 | /** 47 | * Get records by user ids 48 | * 49 | * @param $userIds 50 | * @return UserWorkspaceQuery 51 | */ 52 | public function byUserIds($userIds) 53 | { 54 | return $this->andWhere(['IN', UserWorkspace::tableName() . '.user_id', $userIds]); 55 | } 56 | 57 | /** 58 | * Get records by workspace id 59 | * 60 | * @param $workspaceId 61 | * @return UserWorkspaceQuery 62 | */ 63 | public function byWorkspaceId($workspaceId) 64 | { 65 | return $this->andWhere([UserWorkspace::tableName() . '.workspace_id' => $workspaceId]); 66 | } 67 | } -------------------------------------------------------------------------------- /modules/v1/workspaces/resources/UserCommentResource.php: -------------------------------------------------------------------------------- 1 | function () { 26 | return $this->created_at * 1000; 27 | }, 28 | 'updated_at' => function () { 29 | return $this->updated_at * 1000; 30 | } 31 | ]; 32 | } 33 | 34 | public function extraFields() 35 | { 36 | return ['createdBy', 'updatedBy', 'childrenComments', 'parent']; 37 | } 38 | 39 | /** 40 | * @return ActiveQuery 41 | */ 42 | public function getCreatedBy() 43 | { 44 | return $this->hasOne(UserResource::class, ['id' => 'created_by']); 45 | } 46 | 47 | /** 48 | * @return ActiveQuery 49 | */ 50 | public function getUpdatedBy() 51 | { 52 | return $this->hasOne(UserResource::class, ['id' => 'updated_by']); 53 | } 54 | 55 | /** 56 | * Gets query for [[ChildrenComments]]. 57 | * 58 | * @return ActiveQuery 59 | */ 60 | public function getChildrenComments() 61 | { 62 | return $this->hasMany(UserCommentResource::class, ['parent_id' => 'id'])->orderBy('created_at DESC'); 63 | } 64 | } -------------------------------------------------------------------------------- /modules/v1/users/controllers/UserController.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function actionUpdateProfile() 31 | { 32 | /** @var $user UserProfileResource */ 33 | $user = Yii::$app->user->identity; 34 | $user->image = UploadedFile::getInstanceByName('image'); 35 | if ($user->load(Yii::$app->request->post(), '') && $user->save()) { 36 | return $user; 37 | } 38 | 39 | return $user->getFirstErrors(); 40 | } 41 | 42 | public function actionProfile() 43 | { 44 | return Yii::$app->user->identity; 45 | } 46 | 47 | public function actionChangePassword() 48 | { 49 | $model = new ChangePassword(); 50 | $model->user = Yii::$app->user->identity; 51 | if ($model->load(Yii::$app->request->post(), '') && $model->validate() && $model->changePassword()) { 52 | return $this->response(null, 204); 53 | } 54 | 55 | return $this->validationError($model->getFirstErrors()); 56 | } 57 | } -------------------------------------------------------------------------------- /create-database.php: -------------------------------------------------------------------------------- 1 | exec("CREATE DATABASE IF NOT EXISTS `$DBNAME` DEFAULT CHARACTER SET {$CHARSET} COLLATE {$CHARSET}_unicode_ci;") 40 | or die(print_r($dbh->errorInfo(), true)); 41 | echo "Database \"$DBNAME\" has been successfully created".PHP_EOL; 42 | } catch (PDOException $e) { 43 | die("DB ERROR: " . $e->getMessage()); 44 | } -------------------------------------------------------------------------------- /tests/unit/models/ContactFormTest.php: -------------------------------------------------------------------------------- 1 | model = $this->getMockBuilder('app\models\ContactForm') 20 | ->setMethods(['validate']) 21 | ->getMock(); 22 | 23 | $this->model->expects($this->once()) 24 | ->method('validate') 25 | ->willReturn(true); 26 | 27 | $this->model->attributes = [ 28 | 'name' => 'Tester', 29 | 'email' => 'tester@example.com', 30 | 'subject' => 'very important letter subject', 31 | 'body' => 'body of current message', 32 | ]; 33 | 34 | expect_that($this->model->contact('admin@example.com')); 35 | 36 | // using Yii2 module actions to check email was sent 37 | $this->tester->seeEmailIsSent(); 38 | 39 | /** @var MessageInterface $emailMessage */ 40 | $emailMessage = $this->tester->grabLastSentEmail(); 41 | expect('valid email is sent', $emailMessage)->isInstanceOf('yii\mail\MessageInterface'); 42 | expect($emailMessage->getTo())->hasKey('admin@example.com'); 43 | expect($emailMessage->getFrom())->hasKey('noreply@example.com'); 44 | expect($emailMessage->getReplyTo())->hasKey('tester@example.com'); 45 | expect($emailMessage->getSubject())->equals('very important letter subject'); 46 | expect($emailMessage->toString())->stringContainsString('body of current message'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /views/site/login.php: -------------------------------------------------------------------------------- 1 | title = 'Login'; 11 | $this->params['breadcrumbs'][] = $this->title; 12 | ?> 13 | 48 | -------------------------------------------------------------------------------- /modules/v1/workspaces/models/query/UserCommentQuery.php: -------------------------------------------------------------------------------- 1 | andWhere([UserComment::tableName() . '.id' => $id]); 42 | } 43 | 44 | /** 45 | * @param $parentId 46 | * @return UserCommentQuery 47 | */ 48 | public function byParentId($parentId) 49 | { 50 | return $this->andWhere([UserComment::tableName() . '.parent_id' => $parentId]); 51 | } 52 | 53 | /** 54 | * @param $articleId 55 | * @return UserCommentQuery 56 | */ 57 | public function byArticleId($articleId) 58 | { 59 | return $this->andWhere([UserComment::tableName() . '.article_id' => $articleId]); 60 | } 61 | 62 | /** 63 | * @param $timelinePostId 64 | * @return UserCommentQuery 65 | */ 66 | public function byTimelinePostId($timelinePostId) 67 | { 68 | return $this->andWhere([UserComment::tableName() . '.timeline_post_id' => $timelinePostId]); 69 | } 70 | } -------------------------------------------------------------------------------- /vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | 48 | 77 | -------------------------------------------------------------------------------- /vue/src/modules/Dashboard/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | 53 | 67 | -------------------------------------------------------------------------------- /vue/src/core/services/fileService.js: -------------------------------------------------------------------------------- 1 | const fileService = { 2 | TYPES: {}, 3 | TYPE_VIDEO: 'far fa-file-video', 4 | TYPE_IMAGE: 'far fa-file-image', 5 | TYPE_WORD: 'far fa-file-word', 6 | TYPE_PDF: 'far fa-file-pdf', 7 | TYPE_EXCEL: 'far fa-file-excel', 8 | TYPE_POWERPOINT: 'far fa-file-powerpoint', 9 | 10 | getTypes() { 11 | return { 12 | 'jpg': this.TYPE_IMAGE, 13 | 'png': this.TYPE_IMAGE, 14 | 'jpeg': this.TYPE_IMAGE, 15 | 'svg': this.TYPE_IMAGE, 16 | 'webp' : this.TYPE_IMAGE, 17 | 18 | 'odt': this.TYPE_WORD, 19 | 'doc': this.TYPE_WORD, 20 | 'docx': this.TYPE_WORD, 21 | 22 | 'xls': this.TYPE_EXCEL, 23 | 'xlsx': this.TYPE_EXCEL, 24 | 'xltm': this.TYPE_EXCEL, 25 | 26 | 'ppt': this.TYPE_POWERPOINT, 27 | 'pptx': this.TYPE_POWERPOINT, 28 | 29 | 'pdf': this.TYPE_PDF, 30 | 31 | 'mp4': this.TYPE_VIDEO, 32 | 'avi': this.TYPE_VIDEO, 33 | 'webm': this.TYPE_VIDEO, 34 | 'ogg': this.TYPE_VIDEO, 35 | 'wmv': this.TYPE_VIDEO, 36 | 'mov': this.TYPE_VIDEO, 37 | } 38 | }, 39 | mimeTypes() { 40 | return [ 41 | 'image/png', 42 | 'application/pdf', 43 | 'audio/mp3', 44 | 'video', 45 | 'image/jpeg', 46 | 'image/jpg', 47 | 'image/webp', 48 | 'no-attachment' 49 | ] 50 | }, 51 | isImage(path) { 52 | const ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase(); 53 | return this.getTypes()[ext] === this.TYPE_IMAGE; 54 | }, 55 | isAudio(file) { 56 | return file.mime === 'audio/mp3'; 57 | }, 58 | isVideo(path) { 59 | const ext = path.substring(path.lastIndexOf('.') + 1); 60 | return ext.toLowerCase() === 'mp4'; 61 | }, 62 | isPdf(file) { 63 | return file.mime === 'application/pdf'; 64 | }, 65 | }; 66 | 67 | export default fileService; -------------------------------------------------------------------------------- /tests/functional/LoginFormCest.php: -------------------------------------------------------------------------------- 1 | amOnRoute('site/login'); 8 | } 9 | 10 | public function openLoginPage(\FunctionalTester $I) 11 | { 12 | $I->see('Login', 'h1'); 13 | 14 | } 15 | 16 | // demonstrates `amLoggedInAs` method 17 | public function internalLoginById(\FunctionalTester $I) 18 | { 19 | $I->amLoggedInAs(100); 20 | $I->amOnPage('/'); 21 | $I->see('Logout (admin)'); 22 | } 23 | 24 | // demonstrates `amLoggedInAs` method 25 | public function internalLoginByInstance(\FunctionalTester $I) 26 | { 27 | $I->amLoggedInAs(\app\models\User::findByUsername('admin')); 28 | $I->amOnPage('/'); 29 | $I->see('Logout (admin)'); 30 | } 31 | 32 | public function loginWithEmptyCredentials(\FunctionalTester $I) 33 | { 34 | $I->submitForm('#login-form', []); 35 | $I->expectTo('see validations errors'); 36 | $I->see('Username cannot be blank.'); 37 | $I->see('Password cannot be blank.'); 38 | } 39 | 40 | public function loginWithWrongCredentials(\FunctionalTester $I) 41 | { 42 | $I->submitForm('#login-form', [ 43 | 'LoginForm[username]' => 'admin', 44 | 'LoginForm[password]' => 'wrong', 45 | ]); 46 | $I->expectTo('see validations errors'); 47 | $I->see('Incorrect username or password.'); 48 | } 49 | 50 | public function loginSuccessfully(\FunctionalTester $I) 51 | { 52 | $I->submitForm('#login-form', [ 53 | 'LoginForm[username]' => 'admin', 54 | 'LoginForm[password]' => 'admin', 55 | ]); 56 | $I->see('Logout (admin)'); 57 | $I->dontSeeElement('form#login-form'); 58 | } 59 | } -------------------------------------------------------------------------------- /vue/src/core/scss/mixins/switches.scss: -------------------------------------------------------------------------------- 1 | /*Author : @arboshiki*/ 2 | @mixin switch-size($width, $height, $circle-size, $font-size){ 3 | margin-bottom: (max($height, $circle-size)/2 - 5px); 4 | $offset: ($height - $circle-size) / 2; 5 | input{ 6 | + i{ 7 | width: $width; 8 | height: $height; 9 | margin-bottom: -($height/2 - 5px); 10 | &:before{ 11 | font-size: $font-size; 12 | line-height: $height; 13 | border-radius: $circle-size/2; 14 | font-weight: normal; 15 | } 16 | &:after{ 17 | top: $offset; 18 | left: $offset; 19 | bottom: $offset; 20 | width: $circle-size; 21 | } 22 | } 23 | &:checked{ 24 | + i{ 25 | &:after{ 26 | left: $width - $circle-size - $offset; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | @mixin switch-outline-size($width, $height, $circle-size, $font-size){ 34 | margin-bottom: ($height/2 - 5px); 35 | input{ 36 | + i{ 37 | width: $width; 38 | height: $height; 39 | border-radius: $width/2; 40 | margin-bottom: -($height/2 - 5px); 41 | &:before{ 42 | font-size: $font-size; 43 | line-height: $height - 2 *$switch-outline-border-width; 44 | } 45 | &:after{ 46 | width: $circle-size; 47 | } 48 | } 49 | &:checked{ 50 | +i{ 51 | &:after{ 52 | left: $width - $circle-size - $switch-outline-circle-offset - 2 * $switch-outline-border-width; 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /config/console.php: -------------------------------------------------------------------------------- 1 | 'basic-console', 10 | 'basePath' => dirname(__DIR__), 11 | 'bootstrap' => ['log'], 12 | 'controllerNamespace' => 'app\commands', 13 | 'components' => [ 14 | 'cache' => [ 15 | 'class' => 'yii\caching\FileCache', 16 | ], 17 | 'log' => [ 18 | 'targets' => [ 19 | [ 20 | 'class' => yii\log\DbTarget::class, 21 | 'levels' => ['error', 'warning'], 22 | 'prefix' => function () { 23 | $url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null; 24 | 25 | return sprintf('[%s][%s]', Yii::$app->id, $url); 26 | }, 27 | ], 28 | ], 29 | ], 30 | 'db' => $db, 31 | 'authManager' => [ 32 | 'class' => 'yii\rbac\DbManager', 33 | // uncomment if you want to cache RBAC items hierarchy 34 | 'cache' => 'cache', 35 | ], 36 | ], 37 | 'params' => $params, 38 | /* 39 | 'controllerMap' => [ 40 | 'fixture' => [ // Fixture generation command line. 41 | 'class' => 'yii\faker\FixtureController', 42 | ], 43 | ], 44 | */ 45 | ] 46 | ); 47 | 48 | if (YII_ENV_DEV) { 49 | // configuration adjustments for 'dev' environment 50 | $config['bootstrap'][] = 'gii'; 51 | $config['modules']['gii'] = [ 52 | 'class' => 'yii\gii\Module', 53 | ]; 54 | } 55 | 56 | return $config; 57 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/components/comment/AddComment.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 56 | 57 | -------------------------------------------------------------------------------- /vue/src/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | import {SET_PASSWORD_FORM_LOADING, SET_PROFILE_LOADING, SET_USER} from "@/store/modules/user/mutation-types"; 2 | import httpService from "@/core/services/httpService"; 3 | 4 | /** 5 | * 6 | * @param { function } commit 7 | * @param { object } user 8 | */ 9 | export async function getProfile({commit}) { 10 | commit(SET_PROFILE_LOADING, true); 11 | let response = await httpService.get('/v1/users/user/profile'); 12 | if (response.success) { 13 | commit(SET_USER, response.body); 14 | commit(SET_PROFILE_LOADING, false); 15 | } 16 | return response; 17 | } 18 | 19 | /** 20 | * 21 | * @param { function } commit 22 | * @param { any } user 23 | */ 24 | export async function updateProfile({commit}, user) { 25 | commit(SET_PROFILE_LOADING, true); 26 | const data = prepareData(user); 27 | let response; 28 | if (data instanceof FormData) { 29 | response = await httpService.post('/v1/users/user/update-profile', data) 30 | } else { 31 | response = await httpService.put('/v1/users/user/update-profile', data) 32 | } 33 | const {success, body} = response; 34 | if (success) { 35 | commit(SET_USER, body); 36 | commit(SET_PROFILE_LOADING, false); 37 | } 38 | return response; 39 | } 40 | 41 | export async function changePassword({commit}, passwordData) { 42 | commit(SET_PASSWORD_FORM_LOADING, true); 43 | 44 | const response = await httpService.put('/v1/users/user/change-password', passwordData) 45 | 46 | commit(SET_PASSWORD_FORM_LOADING, false); 47 | return response; 48 | } 49 | 50 | function prepareData(data) { 51 | if (data.image && data.image instanceof File) { 52 | const tmpData = new FormData(); 53 | for (let key in data) { 54 | if (data.hasOwnProperty(key)) { 55 | tmpData.append(key, data[key] || ''); 56 | } 57 | } 58 | data = tmpData; 59 | data.append('_method', 'PUT') 60 | } 61 | return data; 62 | } -------------------------------------------------------------------------------- /vue/public/assets/img/avatar.svg: -------------------------------------------------------------------------------- 1 | profile pic -------------------------------------------------------------------------------- /vue/src/modules/setup/employees/EmployeeModel.js: -------------------------------------------------------------------------------- 1 | import BaseModel from "../../../core/components/input-widget/BaseModel"; 2 | import i18n from "../../../shared/i18n"; 3 | import RoleModel from "@/modules/setup/employees/RoleModel"; 4 | import UserDepartmentModel from "@/modules/setup/employees/UserDepartmentModel"; 5 | 6 | export default class EmployeeModel extends BaseModel { 7 | id = null; 8 | first_name = null; 9 | last_name = null; 10 | mobile = null; 11 | phone = null; 12 | birthday = null; 13 | hobbies = null; 14 | email = null; 15 | userWorkspaces = []; 16 | status = false; 17 | userDepartments = []; 18 | 19 | rules = { 20 | first_name: 'required', 21 | last_name: 'required', 22 | email: [ 23 | {rule: 'required'}, 24 | {rule: 'email'}, 25 | ], 26 | } 27 | 28 | attributeLabels = { 29 | email: i18n.t('Email'), 30 | first_name: i18n.t('First Name'), 31 | last_name: i18n.t('Last Name'), 32 | mobile: i18n.t('Mobile'), 33 | phone: i18n.t('Phone'), 34 | birthday: i18n.t('Birthday'), 35 | hobbies: i18n.t('Hobbies'), 36 | status: i18n.t('Activate User'), 37 | }; 38 | 39 | constructor(data = {}) { 40 | super(); 41 | const userDepartments = []; 42 | 43 | if (data.userDepartments) { 44 | for (let userDepartment of data.userDepartments) { 45 | userDepartments.push(new UserDepartmentModel({ 46 | id: userDepartment.id, 47 | position: userDepartment.position, 48 | country_id: userDepartment.department.country_id, 49 | department_id: userDepartment.department.id 50 | })) 51 | } 52 | } 53 | const userWorkspaces = []; 54 | if (data.userWorkspaces) { 55 | for (let userWorkspace of data.userWorkspaces) { 56 | userWorkspaces.push(new RoleModel({ 57 | id: userWorkspace.id, 58 | role: userWorkspace.role, 59 | workspace_id: userWorkspace.workspace_id, 60 | })) 61 | } 62 | } 63 | data.userDepartments = userDepartments; 64 | data.userWorkspaces = userWorkspaces; 65 | Object.assign(this, {...data}); 66 | } 67 | } -------------------------------------------------------------------------------- /modules/v1/workspaces/resources/FolderResource.php: -------------------------------------------------------------------------------- 1 | function () { 35 | return $this->getFileUrl(); 36 | }, 37 | 'mime' => function () { 38 | return $this->mime; 39 | }, 40 | 'created_at' => function () { 41 | return $this->created_at * 1000; 42 | }, 43 | 'updated_at' => function () { 44 | return $this->updated_at * 1000; 45 | }, 46 | ]; 47 | } 48 | 49 | /** 50 | * @return array|string[] 51 | */ 52 | public function extraFields(): array 53 | { 54 | return [ 55 | 'children', 56 | 'workspace', 57 | 'createdBy', 58 | 'updatedBy', 59 | ]; 60 | } 61 | 62 | /** 63 | * Gets query for [[CreatedBy]]. 64 | * 65 | * @return ActiveQuery 66 | */ 67 | public function getCreatedBy() 68 | { 69 | return $this->hasOne(UserResource::class, ['id' => 'created_by']); 70 | } 71 | 72 | /** 73 | * Gets query for [[UpdatedBy]]. 74 | * 75 | * @return ActiveQuery|UserQuery 76 | */ 77 | public function getUpdatedBy(): UserQuery 78 | { 79 | return $this->hasOne(UserResource::class, ['id' => 'updated_by']); 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /vue/src/modules/Workspace/components/comment/CommentItem.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 49 | 50 | -------------------------------------------------------------------------------- /vue/src/modules/setup/employees/EmployeeList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 63 | 64 | -------------------------------------------------------------------------------- /vue/src/modules/Workspace/view/about/WorkspaceAbout.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 56 | 57 | -------------------------------------------------------------------------------- /vue/src/modules/setup/countries/CountryModal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /vue/src/plugins/bootstrap/bootstrap.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | 8 | @import "node_modules/bootstrap/scss/functions"; 9 | @import "variables"; 10 | @import "node_modules/bootstrap/scss/mixins"; 11 | @import "node_modules/bootstrap/scss/root"; 12 | @import "node_modules/bootstrap/scss/reboot"; 13 | @import "node_modules/bootstrap/scss/type"; 14 | @import "node_modules/bootstrap/scss/images"; 15 | @import "node_modules/bootstrap/scss/code"; 16 | @import "node_modules/bootstrap/scss/grid"; 17 | @import "node_modules/bootstrap/scss/tables"; 18 | @import "node_modules/bootstrap/scss/forms"; 19 | @import "node_modules/bootstrap/scss/buttons"; 20 | @import "node_modules/bootstrap/scss/transitions"; 21 | @import "node_modules/bootstrap/scss/dropdown"; 22 | @import "node_modules/bootstrap/scss/button-group"; 23 | @import "node_modules/bootstrap/scss/input-group"; 24 | @import "node_modules/bootstrap/scss/custom-forms"; 25 | @import "node_modules/bootstrap/scss/nav"; 26 | @import "node_modules/bootstrap/scss/navbar"; 27 | @import "node_modules/bootstrap/scss/card"; 28 | @import "node_modules/bootstrap/scss/breadcrumb"; 29 | @import "node_modules/bootstrap/scss/pagination"; 30 | @import "node_modules/bootstrap/scss/badge"; 31 | @import "node_modules/bootstrap/scss/jumbotron"; 32 | @import "node_modules/bootstrap/scss/alert"; 33 | @import "node_modules/bootstrap/scss/progress"; 34 | @import "node_modules/bootstrap/scss/media"; 35 | @import "node_modules/bootstrap/scss/list-group"; 36 | @import "node_modules/bootstrap/scss/close"; 37 | @import "node_modules/bootstrap/scss/toasts"; 38 | @import "node_modules/bootstrap/scss/modal"; 39 | @import "node_modules/bootstrap/scss/tooltip"; 40 | @import "node_modules/bootstrap/scss/popover"; 41 | @import "node_modules/bootstrap/scss/carousel"; 42 | @import "node_modules/bootstrap/scss/spinners"; 43 | @import "node_modules/bootstrap/scss/utilities"; 44 | @import "node_modules/bootstrap/scss/print"; 45 | 46 | @import 'node_modules/bootstrap-vue/src/index.scss'; 47 | @import '../../core/scss/bootstrap'; 48 | --------------------------------------------------------------------------------