From feac5ab6f36ebcc50d7111322281ba9451368fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Wed, 21 Jun 2023 14:53:03 +0800 Subject: [PATCH 01/54] =?UTF-8?q?refactor:=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dept_controller.py | 8 ++--- .../controller/login_controller.py | 8 ++--- .../controller/menu_controller.py | 8 ++--- .../module_admin/controller/post_controler.py | 6 ++-- .../controller/role_controller.py | 6 ++-- .../controller/user_controller.py | 8 ++--- .../module_admin/dao/dept_dao.py | 12 ++++---- .../module_admin/dao/login_dao.py | 4 +-- .../module_admin/dao/menu_dao.py | 12 ++++---- .../module_admin/dao/post_dao.py | 16 +++++----- .../module_admin/dao/role_dao.py | 22 +++++++------- .../module_admin/dao/user_dao.py | 30 +++++++++---------- .../module_admin/entity/vo/dept_vo.py | 2 +- .../module_admin/entity/vo/post_vo.py | 2 +- .../module_admin/entity/vo/role_vo.py | 4 +-- .../module_admin/service/dept_service.py | 12 ++++---- .../module_admin/service/login_service.py | 10 +++---- .../module_admin/service/menu_service.py | 10 +++---- .../module_admin/service/post_service.py | 12 ++++---- .../module_admin/service/role_service.py | 20 ++++++------- .../module_admin/service/user_service.py | 26 ++++++++-------- 21 files changed, 119 insertions(+), 119 deletions(-) diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 69c87a5..186d2bb 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.dept_service import * -from module_admin.entity.vo.dept_schema import * -from module_admin.mapper.dept_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.dept_vo import * +from module_admin.dao.dept_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * deptController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index c934add..7d10809 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -1,11 +1,11 @@ import uuid from fastapi import APIRouter from module_admin.service.login_service import * -from module_admin.entity.vo.login_schema import * -from module_admin.mapper.login_crud import * +from module_admin.entity.vo.login_vo import * +from module_admin.dao.login_dao import * from config.env import JwtConfig -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * from datetime import timedelta diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 70e273a..4dc107c 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.menu_service import * -from module_admin.entity.vo.menu_schema import * -from module_admin.mapper.menu_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.menu_vo import * +from module_admin.dao.menu_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * menuController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index c6d9614..f2fac53 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -3,9 +3,9 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.post_service import * -from module_admin.entity.vo.post_schema import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.post_vo import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * postController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 7d53d83..fbcac03 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -3,9 +3,9 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.role_service import * -from module_admin.entity.vo.role_schema import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.role_vo import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * roleController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index dbaf254..2bfb939 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user, get_password_hash from module_admin.service.user_service import * -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.user_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.user_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * userController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/dao/dept_dao.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py index e2ce754..f4815d3 100644 --- a/dash-fastapi-backend/module_admin/dao/dept_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dept_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.dept_entity import SysDept -from module_admin.entity.vo.dept_schema import DeptModel, DeptResponse, CrudDeptResponse -from module_admin.utils.time_format_tool import list_format_datetime +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.vo.dept_vo import DeptModel, DeptResponse, CrudDeptResponse +from module_admin.utils.time_format_util import list_format_datetime def get_dept_by_id(db: Session, dept_id: int): @@ -126,7 +126,7 @@ def get_dept_list(db: Session, page_object: DeptModel): return DeptResponse(**result) -def add_dept_crud(db: Session, dept: DeptModel): +def add_dept_dao(db: Session, dept: DeptModel): """ 新增部门数据库操作 :param db: orm对象 @@ -142,7 +142,7 @@ def add_dept_crud(db: Session, dept: DeptModel): return CrudDeptResponse(**result) -def edit_dept_crud(db: Session, dept: dict): +def edit_dept_dao(db: Session, dept: dict): """ 编辑部门数据库操作 :param db: orm对象 @@ -162,7 +162,7 @@ def edit_dept_crud(db: Session, dept: dict): return CrudDeptResponse(**result) -def delete_dept_crud(db: Session, dept: DeptModel): +def delete_dept_dao(db: Session, dept: DeptModel): """ 删除部门数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/login_dao.py b/dash-fastapi-backend/module_admin/dao/login_dao.py index 0e39974..e723c2e 100644 --- a/dash-fastapi-backend/module_admin/dao/login_dao.py +++ b/dash-fastapi-backend/module_admin/dao/login_dao.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.user_entity import SysUser -from module_admin.utils.time_format_tool import object_format_datetime +from module_admin.entity.do.user_do import SysUser +from module_admin.utils.time_format_util import object_format_datetime def login_by_account(db: Session, user_name: str): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 50749e8..421fbd5 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.menu_schema import MenuModel, MenuResponse, CrudMenuResponse -from module_admin.utils.time_format_tool import list_format_datetime +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse +from module_admin.utils.time_format_util import list_format_datetime def get_menu_detail_by_id(db: Session, menu_id: int): @@ -56,7 +56,7 @@ def get_menu_list(db: Session, page_object: MenuModel): return MenuResponse(**result) -def add_menu_crud(db: Session, menu: MenuModel): +def add_menu_dao(db: Session, menu: MenuModel): """ 新增菜单数据库操作 :param db: orm对象 @@ -72,7 +72,7 @@ def add_menu_crud(db: Session, menu: MenuModel): return CrudMenuResponse(**result) -def edit_menu_crud(db: Session, menu: dict): +def edit_menu_dao(db: Session, menu: dict): """ 编辑菜单数据库操作 :param db: orm对象 @@ -92,7 +92,7 @@ def edit_menu_crud(db: Session, menu: dict): return CrudMenuResponse(**result) -def delete_menu_crud(db: Session, menu: MenuModel): +def delete_menu_dao(db: Session, menu: MenuModel): """ 删除菜单数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index 8b04c4f..4b3a841 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.post_entity import SysPost -from module_admin.entity.vo.post_schema import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse -from module_admin.utils.time_format_tool import list_format_datetime -from module_admin.utils.page_tool import get_page_info +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse +from module_admin.utils.time_format_util import list_format_datetime +from module_admin.utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): @@ -22,7 +22,7 @@ def get_post_detail_by_id(db: Session, post_id: int): return post_info -def get_post_select_option_crud(db: Session): +def get_post_select_option_dao(db: Session): post_info = db.query(SysPost) \ .filter(SysPost.status == 0) \ .all() @@ -67,7 +67,7 @@ def get_post_list(db: Session, page_object: PostPageObject): return PostPageObjectResponse(**result) -def add_post_crud(db: Session, post: PostModel): +def add_post_dao(db: Session, post: PostModel): """ 新增岗位数据库操作 :param db: orm对象 @@ -83,7 +83,7 @@ def add_post_crud(db: Session, post: PostModel): return CrudPostResponse(**result) -def edit_post_crud(db: Session, post: dict): +def edit_post_dao(db: Session, post: dict): """ 编辑岗位数据库操作 :param db: orm对象 @@ -103,7 +103,7 @@ def edit_post_crud(db: Session, post: dict): return CrudPostResponse(**result) -def delete_post_crud(db: Session, post: PostModel): +def delete_post_dao(db: Session, post: PostModel): """ 删除岗位数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 6923dd5..45d5edb 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -1,10 +1,10 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from module_admin.entity.do.role_entity import SysRole, SysRoleMenu -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.role_schema import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel -from module_admin.utils.time_format_tool import list_format_datetime, object_format_datetime -from module_admin.utils.page_tool import get_page_info +from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel +from module_admin.utils.time_format_util import list_format_datetime, object_format_datetime +from module_admin.utils.page_util import get_page_info from datetime import datetime, time @@ -55,7 +55,7 @@ def get_role_detail_by_id(db: Session, role_id: int): return RoleDetailModel(**results) -def get_role_select_option_crud(db: Session): +def get_role_select_option_dao(db: Session): role_info = db.query(SysRole) \ .filter(SysRole.status == 0, SysRole.del_flag == 0) \ .all() @@ -110,7 +110,7 @@ def get_role_list(db: Session, page_object: RolePageObject): return RolePageObjectResponse(**result) -def add_role_crud(db: Session, role: RoleModel): +def add_role_dao(db: Session, role: RoleModel): """ 新增角色数据库操作 :param db: orm对象 @@ -126,7 +126,7 @@ def add_role_crud(db: Session, role: RoleModel): return CrudRoleResponse(**result) -def edit_role_crud(db: Session, role: dict): +def edit_role_dao(db: Session, role: dict): """ 编辑角色数据库操作 :param db: orm对象 @@ -146,7 +146,7 @@ def edit_role_crud(db: Session, role: dict): return CrudRoleResponse(**result) -def delete_role_crud(db: Session, role: RoleModel): +def delete_role_dao(db: Session, role: RoleModel): """ 删除角色数据库操作 :param db: orm对象 @@ -159,7 +159,7 @@ def delete_role_crud(db: Session, role: RoleModel): db.commit() # 提交保存到数据库中 -def add_role_menu_crud(db: Session, role_menu: RoleMenuModel): +def add_role_menu_dao(db: Session, role_menu: RoleMenuModel): """ 新增角色菜单关联信息数据库操作 :param db: orm对象 @@ -172,7 +172,7 @@ def add_role_menu_crud(db: Session, role_menu: RoleMenuModel): db.refresh(db_role_menu) # 刷新 -def delete_role_menu_crud(db: Session, role_menu: RoleMenuModel): +def delete_role_menu_dao(db: Session, role_menu: RoleMenuModel): """ 删除角色菜单关联信息数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index 81da538..efc9682 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -1,14 +1,14 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from module_admin.entity.do.user_entity import SysUser, SysUserRole, SysUserPost -from module_admin.entity.do.role_entity import SysRole, SysRoleMenu -from module_admin.entity.do.dept_entity import SysDept -from module_admin.entity.do.post_entity import SysPost -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ +from module_admin.entity.do.user_do import SysUser, SysUserRole, SysUserPost +from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ UserPageObjectResponse, CrudUserResponse -from module_admin.utils.time_format_tool import list_format_datetime, format_datetime_dict_list -from module_admin.utils.page_tool import get_page_info +from module_admin.utils.time_format_util import list_format_datetime, format_datetime_dict_list +from module_admin.utils.page_util import get_page_info from datetime import datetime, time @@ -191,7 +191,7 @@ def get_user_list(db: Session, page_object: UserPageObject): return UserPageObjectResponse(**result) -def add_user_crud(db: Session, user: UserModel): +def add_user_dao(db: Session, user: UserModel): """ 新增用户数据库操作 :param db: orm对象 @@ -211,7 +211,7 @@ def add_user_crud(db: Session, user: UserModel): return CrudUserResponse(**result) -def edit_user_crud(db: Session, user: dict): +def edit_user_dao(db: Session, user: dict): """ 编辑用户数据库操作 :param db: orm对象 @@ -234,7 +234,7 @@ def edit_user_crud(db: Session, user: dict): return CrudUserResponse(**result) -def delete_user_crud(db: Session, user: UserModel): +def delete_user_dao(db: Session, user: UserModel): """ 删除用户数据库操作 :param db: orm对象 @@ -247,7 +247,7 @@ def delete_user_crud(db: Session, user: UserModel): db.commit() # 提交保存到数据库中 -def add_user_role_crud(db: Session, user_role: UserRoleModel): +def add_user_role_dao(db: Session, user_role: UserRoleModel): """ 新增用户角色关联信息数据库操作 :param db: orm对象 @@ -260,7 +260,7 @@ def add_user_role_crud(db: Session, user_role: UserRoleModel): db.refresh(db_user_role) # 刷新 -def delete_user_role_crud(db: Session, user_role: UserRoleModel): +def delete_user_role_dao(db: Session, user_role: UserRoleModel): """ 删除用户角色关联信息数据库操作 :param db: orm对象 @@ -273,7 +273,7 @@ def delete_user_role_crud(db: Session, user_role: UserRoleModel): db.commit() # 提交保存到数据库中 -def add_user_post_crud(db: Session, user_post: UserPostModel): +def add_user_post_dao(db: Session, user_post: UserPostModel): """ 新增用户岗位关联信息数据库操作 :param db: orm对象 @@ -286,7 +286,7 @@ def add_user_post_crud(db: Session, user_post: UserPostModel): db.refresh(db_user_post) # 刷新 -def delete_user_post_crud(db: Session, user_post: UserPostModel): +def delete_user_post_dao(db: Session, user_post: UserPostModel): """ 删除用户岗位关联信息数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py b/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py index 2a72eb0..a2cf228 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import DeptModel +from module_admin.entity.vo.user_vo import DeptModel class DeptPageObject(DeptModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/post_vo.py b/dash-fastapi-backend/module_admin/entity/vo/post_vo.py index c2edd89..5c39e88 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/post_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/post_vo.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import PostModel +from module_admin.entity.vo.user_vo import PostModel class PostPageObject(PostModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py index 68a7d9e..c31322d 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import RoleModel -from module_admin.entity.vo.menu_schema import MenuModel +from module_admin.entity.vo.user_vo import RoleModel +from module_admin.entity.vo.menu_vo import MenuModel class RoleMenuModel(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/dept_service.py b/dash-fastapi-backend/module_admin/service/dept_service.py index c86ad40..682706e 100644 --- a/dash-fastapi-backend/module_admin/service/dept_service.py +++ b/dash-fastapi-backend/module_admin/service/dept_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.dept_schema import * -from module_admin.mapper.dept_crud import * +from module_admin.entity.vo.dept_vo import * +from module_admin.dao.dept_dao import * def get_dept_tree_services(result_db: Session, page_object: DeptModel): @@ -52,7 +52,7 @@ def add_dept_services(result_db: Session, page_object: DeptModel): page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' else: page_object.ancestors = '0' - add_dept_result = add_dept_crud(result_db, page_object) + add_dept_result = add_dept_dao(result_db, page_object) return add_dept_result @@ -70,7 +70,7 @@ def edit_dept_services(result_db: Session, page_object: DeptModel): else: page_object.ancestors = '0' edit_dept = page_object.dict(exclude_unset=True) - edit_dept_result = edit_dept_crud(result_db, edit_dept) + edit_dept_result = edit_dept_dao(result_db, edit_dept) update_children_info(result_db, DeptModel(dept_id=page_object.dept_id, ancestors=page_object.ancestors, update_by=page_object.update_by, @@ -99,7 +99,7 @@ def delete_dept_services(result_db: Session, page_object: DeleteDeptModel): return CrudDeptResponse(**result) dept_id_dict = dict(dept_id=dept_id) - delete_dept_crud(result_db, DeptModel(**dept_id_dict)) + delete_dept_dao(result_db, DeptModel(**dept_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') @@ -155,7 +155,7 @@ def update_children_info(result_db, page_object): if children_info: for child in children_info: child.ancestors = f'{page_object.ancestors},{page_object.dept_id}' - edit_dept_crud(result_db, + edit_dept_dao(result_db, dict(dept_id=child.dept_id, ancestors=child.ancestors, update_by=page_object.update_by, diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index 29deaef..d098017 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -1,11 +1,11 @@ -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.login_crud import * -from module_admin.mapper.user_crud import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.login_dao import * +from module_admin.dao.user_dao import * from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * from datetime import datetime, timedelta from fastapi import Request from fastapi import Depends, Header diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index f3465ec..2c102b5 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.menu_schema import * -from module_admin.mapper.menu_crud import * +from module_admin.entity.vo.menu_vo import * +from module_admin.dao.menu_dao import * def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): @@ -54,7 +54,7 @@ def add_menu_services(result_db: Session, page_object: MenuModel): :param page_object: 新增菜单对象 :return: 新增菜单校验结果 """ - add_menu_result = add_menu_crud(result_db, page_object) + add_menu_result = add_menu_dao(result_db, page_object) return add_menu_result @@ -67,7 +67,7 @@ def edit_menu_services(result_db: Session, page_object: MenuModel): :return: 编辑菜单校验结果 """ edit_menu = page_object.dict(exclude_unset=True) - edit_menu_result = edit_menu_crud(result_db, edit_menu) + edit_menu_result = edit_menu_dao(result_db, edit_menu) return edit_menu_result @@ -83,7 +83,7 @@ def delete_menu_services(result_db: Session, page_object: DeleteMenuModel): menu_id_list = page_object.menu_ids.split(',') for menu_id in menu_id_list: menu_id_dict = dict(menu_id=menu_id) - delete_menu_crud(result_db, MenuModel(**menu_id_dict)) + delete_menu_dao(result_db, MenuModel(**menu_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index 6151ed8..f048a05 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.post_schema import * -from module_admin.mapper.post_crud import * +from module_admin.entity.vo.post_vo import * +from module_admin.dao.post_dao import * def get_post_select_option_services(result_db: Session): @@ -8,7 +8,7 @@ def get_post_select_option_services(result_db: Session): :param result_db: orm对象 :return: 岗位列表不分页信息对象 """ - post_list_result = get_post_select_option_crud(result_db) + post_list_result = get_post_select_option_dao(result_db) return post_list_result @@ -32,7 +32,7 @@ def add_post_services(result_db: Session, page_object: PostModel): :param page_object: 新增岗位对象 :return: 新增岗位校验结果 """ - add_post_result = add_post_crud(result_db, page_object) + add_post_result = add_post_dao(result_db, page_object) return add_post_result @@ -45,7 +45,7 @@ def edit_post_services(result_db: Session, page_object: PostModel): :return: 编辑岗位校验结果 """ edit_post = page_object.dict(exclude_unset=True) - edit_post_result = edit_post_crud(result_db, edit_post) + edit_post_result = edit_post_dao(result_db, edit_post) return edit_post_result @@ -61,7 +61,7 @@ def delete_post_services(result_db: Session, page_object: DeletePostModel): post_id_list = page_object.post_ids.split(',') for post_id in post_id_list: post_id_dict = dict(post_id=post_id) - delete_post_crud(result_db, PostModel(**post_id_dict)) + delete_post_dao(result_db, PostModel(**post_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') diff --git a/dash-fastapi-backend/module_admin/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py index ff0b1e9..ed30123 100644 --- a/dash-fastapi-backend/module_admin/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.role_schema import * -from module_admin.mapper.role_crud import * +from module_admin.entity.vo.role_vo import * +from module_admin.dao.role_dao import * def get_role_select_option_services(result_db: Session): @@ -8,7 +8,7 @@ def get_role_select_option_services(result_db: Session): :param result_db: orm对象 :return: 角色列表不分页信息对象 """ - role_list_result = get_role_select_option_crud(result_db) + role_list_result = get_role_select_option_dao(result_db) return role_list_result @@ -33,14 +33,14 @@ def add_role_services(result_db: Session, page_object: AddRoleModel): :return: 新增角色校验结果 """ add_role = RoleModel(**page_object.dict()) - add_role_result = add_role_crud(result_db, add_role) + add_role_result = add_role_dao(result_db, add_role) if add_role_result.is_success: role_id = get_role_by_name(result_db, page_object.role_name).role_id if page_object.menu_id: menu_id_list = page_object.menu_id.split(',') for menu in menu_id_list: menu_dict = dict(role_id=role_id, menu_id=menu) - add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + add_role_menu_dao(result_db, RoleMenuModel(**menu_dict)) return add_role_result @@ -57,15 +57,15 @@ def edit_role_services(result_db: Session, page_object: AddRoleModel): del edit_role['menu_id'] if page_object.type == 'status': del edit_role['type'] - edit_role_result = edit_role_crud(result_db, edit_role) + edit_role_result = edit_role_dao(result_db, edit_role) if edit_role_result.is_success and page_object.type != 'status': role_id_dict = dict(role_id=page_object.role_id) - delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) + delete_role_menu_dao(result_db, RoleMenuModel(**role_id_dict)) if page_object.menu_id: menu_id_list = page_object.menu_id.split(',') for menu in menu_id_list: menu_dict = dict(role_id=page_object.role_id, menu_id=menu) - add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + add_role_menu_dao(result_db, RoleMenuModel(**menu_dict)) return edit_role_result @@ -81,8 +81,8 @@ def delete_role_services(result_db: Session, page_object: DeleteRoleModel): role_id_list = page_object.role_ids.split(',') for role_id in role_id_list: role_id_dict = dict(role_id=role_id, update_by=page_object.update_by, update_time=page_object.update_time) - delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) - delete_role_crud(result_db, RoleModel(**role_id_dict)) + delete_role_menu_dao(result_db, RoleMenuModel(**role_id_dict)) + delete_role_dao(result_db, RoleModel(**role_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入角色id为空') diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 909f5b3..230de75 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.user_crud import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.user_dao import * def get_user_list_services(result_db: Session, page_object: UserPageObject): @@ -22,19 +22,19 @@ def add_user_services(result_db: Session, page_object: AddUserModel): :return: 新增用户校验结果 """ add_user = UserModel(**page_object.dict()) - add_user_result = add_user_crud(result_db, add_user) + add_user_result = add_user_dao(result_db, add_user) if add_user_result.is_success: user_id = get_user_by_name(result_db, page_object.user_name).user_id if page_object.role_id: role_id_list = page_object.role_id.split(',') for role in role_id_list: role_dict = dict(user_id=user_id, role_id=role) - add_user_role_crud(result_db, UserRoleModel(**role_dict)) + add_user_role_dao(result_db, UserRoleModel(**role_dict)) if page_object.post_id: post_id_list = page_object.post_id.split(',') for post in post_id_list: post_dict = dict(user_id=user_id, post_id=post) - add_user_post_crud(result_db, UserPostModel(**post_dict)) + add_user_post_dao(result_db, UserPostModel(**post_dict)) return add_user_result @@ -52,21 +52,21 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): del edit_user['post_id'] if page_object.type == 'status': del edit_user['type'] - edit_user_result = edit_user_crud(result_db, edit_user) + edit_user_result = edit_user_dao(result_db, edit_user) if edit_user_result.is_success and page_object.type != 'status': user_id_dict = dict(user_id=page_object.user_id) - delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) - delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) + delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) if page_object.role_id: role_id_list = page_object.role_id.split(',') for role in role_id_list: role_dict = dict(user_id=page_object.user_id, role_id=role) - add_user_role_crud(result_db, UserRoleModel(**role_dict)) + add_user_role_dao(result_db, UserRoleModel(**role_dict)) if page_object.post_id: post_id_list = page_object.post_id.split(',') for post in post_id_list: post_dict = dict(user_id=page_object.user_id, post_id=post) - add_user_post_crud(result_db, UserPostModel(**post_dict)) + add_user_post_dao(result_db, UserPostModel(**post_dict)) return edit_user_result @@ -82,9 +82,9 @@ def delete_user_services(result_db: Session, page_object: DeleteUserModel): user_id_list = page_object.user_ids.split(',') for user_id in user_id_list: user_id_dict = dict(user_id=user_id, update_by=page_object.update_by, update_time=page_object.update_time) - delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) - delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) - delete_user_crud(result_db, UserModel(**user_id_dict)) + delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) + delete_user_dao(result_db, UserModel(**user_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') -- Gitee From 16b0bafcefc8915a838d38e31ad4df57ddcb8a52 Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 6 Jul 2023 16:29:06 +0800 Subject: [PATCH 02/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=BA=A7=E5=88=AB=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6=20fix:?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=8F=9C=E5=8D=95=E9=9A=90=E8=97=8F=E6=97=A0?= =?UTF-8?q?=E6=95=88=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/service/login_service.py | 4 +- dash-fastapi-frontend/app.py | 41 ++- .../callbacks/layout_c/index_c.py | 20 +- .../callbacks/system_c/dept_c.py | 11 +- .../callbacks/system_c/menu_c/menu_c.py | 11 +- .../callbacks/system_c/post_c.py | 11 +- .../callbacks/system_c/role_c.py | 56 +++- .../callbacks/system_c/user_c.py | 11 +- dash-fastapi-frontend/store/store.py | 4 + dash-fastapi-frontend/utils/tree_tool.py | 50 +++ .../views/system/config/__init__.py | 2 +- .../views/system/dept/__init__.py | 155 +++++---- .../views/system/dict/__init__.py | 2 +- .../views/system/menu/__init__.py | 11 +- .../views/system/notice/__init__.py | 2 +- .../views/system/post/__init__.py | 256 +++++++------- .../views/system/role/__init__.py | 274 ++++++++------- .../views/system/user/__init__.py | 313 ++++++++++-------- 18 files changed, 721 insertions(+), 513 deletions(-) diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index d098017..49be7ca 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -51,14 +51,14 @@ async def get_current_user(request: Request = Request, token: str = Header(...), # dept_name=user.user_dept_info[0].dept_name, # ancestors=user.user_dept_info[0].ancestors)) # user_role_info = deal_user_role_info(RoleInfo(role_info=user.user_role_info)) - user_menu_info = deal_user_menu_info(0, MenuList(menu_info=user.user_menu_info)) + # user_menu_info = deal_user_menu_info(0, MenuList(menu_info=user.user_menu_info)) return CurrentUserInfoServiceResponse( user=user.user_basic_info[0], dept=user.user_dept_info[0], role=user.user_role_info, post=user.user_post_info, - menu=user_menu_info + menu=user.user_menu_info ) else: logger.warning("用户token已失效,请重新登录") diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index 6c5a23a..c827f04 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -15,7 +15,7 @@ import views from callbacks import app_c from api.login import get_current_user_info_api -from utils.tree_tool import find_node_values, find_key_by_href +from utils.tree_tool import find_node_values, find_key_by_href, deal_user_menu_info app.layout = html.Div( [ @@ -66,7 +66,9 @@ app.layout = html.Div( Output('redirect-container', 'children', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), - Output('current-key-container', 'data')], + Output('current-key-container', 'data'), + Output('menu-info-store-container', 'data'), + Output('menu-list-store-container', 'data')], Input('url-container', 'pathname'), State('url-container', 'trigger'), prevent_initial_call=True @@ -83,12 +85,14 @@ def router(pathname, trigger): user_name = current_user['user']['user_name'] nick_name = current_user['user']['nick_name'] phone_number = current_user['user']['phonenumber'] - menu_info = current_user['menu'] + menu_list = current_user['menu'] + user_menu_list = [item for item in menu_list if item.get('visible') == '0'] + menu_info = deal_user_menu_info(0, menu_list) + user_menu_info = deal_user_menu_info(0, user_menu_list) session['user_info'] = current_user['user'] session['dept_info'] = current_user['dept'] session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] - session['menu_info'] = menu_info valid_href_list = find_node_values(menu_info, 'href') valid_href_list.append('/') if pathname in valid_href_list: @@ -107,16 +111,21 @@ def router(pathname, trigger): id='router-redirect' ), None, - {'timestamp': time.time()} + {'timestamp': time.time()}, + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # 否则正常渲染主页面 return [ - views.layout.render_content(user_name, nick_name, phone_number, menu_info), + views.layout.render_content(user_name, nick_name, phone_number, user_menu_info), None, fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), {'timestamp': time.time()}, - {'current_key': current_key} + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # elif trigger == 'pushstate': @@ -128,7 +137,9 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, - {'current_key': current_key} + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # else: @@ -148,6 +159,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -157,6 +170,8 @@ def router(pathname, trigger): dash.no_update, dash.no_update, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -168,6 +183,8 @@ def router(pathname, trigger): None, fuc.FefferyFancyNotification('接口异常', type='error', autoClose=2000), {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] else: @@ -181,6 +198,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -190,6 +209,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -199,6 +220,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -211,6 +234,8 @@ def router(pathname, trigger): ), None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 15ec34b..91bb76a 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -18,10 +18,12 @@ from utils.tree_tool import find_title_by_key, find_modules_by_key, find_href_by [Input('index-side-menu', 'currentKey'), Input('tabs-container', 'latestDeletePane')], [State('tabs-container', 'items'), - State('tabs-container', 'activeKey')], + State('tabs-container', 'activeKey'), + State('menu-info-store-container', 'data'), + State('menu-list-store-container', 'data')], prevent_initial_call=True ) -def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, activeKey): +def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, activeKey, menu_info, menu_list): """ 这个回调函数用于处理标签页子项的新建、切换及删除 具体策略: @@ -47,9 +49,10 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act currentKey ] - menu_title = find_title_by_key(session.get('menu_info'), currentKey) + menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) + button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 - menu_modules = find_modules_by_key(session.get('menu_info'), currentKey) + menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) if menu_modules: # 否则追加子项返回 @@ -60,7 +63,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act { 'label': menu_title, 'key': currentKey, - 'children': eval('views.' + menu_modules + '.render()'), + 'children': eval('views.' + menu_modules + '.render(button_perms)'), } ], currentKey @@ -115,9 +118,10 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act [Output('header-breadcrumb', 'items'), Output('dcc-url', 'pathname')], Input('tabs-container', 'activeKey'), + State('menu-info-store-container', 'data'), prevent_initial_call=True ) -def get_current_breadcrumbs(active_key): +def get_current_breadcrumbs(active_key, menu_info): if active_key: if active_key == '首页': @@ -133,11 +137,11 @@ def get_current_breadcrumbs(active_key): ] else: - result = find_parents(session.get('menu_info'), active_key) + result = find_parents(menu_info.get('menu_info'), active_key) # 去除result的重复项 parent_info = list(OrderedDict((json.dumps(d, ensure_ascii=False), d) for d in result).values()) if parent_info: - current_href = find_href_by_key(session.get('menu_info'), active_key) + current_href = find_href_by_key(menu_info.get('menu_info'), active_key) return [ [ diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index a9497f7..028218e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -26,10 +26,11 @@ from api.dept import get_dept_tree_api, get_dept_list_api, add_dept_api, edit_de Input('dept-fold', 'nClicks')], [State('dept-dept_name-input', 'value'), State('dept-status-select', 'value'), - State('dept-list-table', 'defaultExpandedRowKeys')], + State('dept-list-table', 'defaultExpandedRowKeys'), + State('dept-button-perms-container', 'data')], prevent_initial_call=True ) -def get_dept_table_data(search_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys): +def get_dept_table_data(search_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys, button_perms): query_params = dict( dept_name=dept_name, @@ -55,17 +56,17 @@ def get_dept_table_data(search_click, operations, fold_click, dept_name, status_ 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:dept:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:dept:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:dept:remove' in button_perms else {}, ] table_data_new = get_dept_tree(0, table_data) diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index d68af1b..b5dc97e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -26,10 +26,11 @@ from api.menu import get_menu_tree_api, get_menu_tree_for_edit_option_api, get_m Input('menu-fold', 'nClicks')], [State('menu-menu_name-input', 'value'), State('menu-status-select', 'value'), - State('menu-list-table', 'defaultExpandedRowKeys')], + State('menu-list-table', 'defaultExpandedRowKeys'), + State('menu-button-perms-container', 'data')], prevent_initial_call=True ) -def get_menu_table_data(search_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys): +def get_menu_table_data(search_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys, button_perms): query_params = dict( menu_name=menu_name, @@ -62,17 +63,17 @@ def get_menu_table_data(search_click, operations, fold_click, menu_name, status_ 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:menu:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:menu:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:menu:remove' in button_perms else {}, ] table_data_new = list_to_tree(table_data) diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index b071aeb..33c21c3 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -20,10 +20,11 @@ from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_ Input('post-operations-store', 'data')], [State('post-post_code-input', 'value'), State('post-post_name-input', 'value'), - State('post-status-select', 'value')], + State('post-status-select', 'value'), + State('post-button-perms-container', 'data')], prevent_initial_call=True ) -def get_post_table_data(search_click, pagination, operations, post_code, post_name, status_select): +def get_post_table_data(search_click, pagination, operations, post_code, post_name, status_select, button_perms): query_params = dict( post_code=post_code, @@ -63,12 +64,12 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:post:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:post:remove' in button_perms else {}, ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] @@ -290,7 +291,7 @@ def post_delete_modal(delete_click, button_click, return dash.no_update return [ - f'是否确认删除岗位编号为{post_ids}的用户?', + f'是否确认删除岗位编号为{post_ids}的岗位?', True, {'post_ids': post_ids} ] diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index f11b72f..69982c6 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -22,10 +22,11 @@ from api.menu import get_menu_tree_api [State('role-role_name-input', 'value'), State('role-role_key-input', 'value'), State('role-status-select', 'value'), - State('role-create_time-range', 'value')], + State('role-create_time-range', 'value'), + State('role-button-perms-container', 'data')], prevent_initial_call=True ) -def get_role_table_data(search_click, pagination, operations, role_name, role_key, status_select, create_time_range): +def get_role_table_data(search_click, pagination, operations, role_name, role_key, status_select, create_time_range, button_perms): create_time_start = None create_time_end = None @@ -74,12 +75,12 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:role:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:role:remove' in button_perms else {}, ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] @@ -143,8 +144,8 @@ def fold_unfold_role_menu(fold_unfold, menu_info): @app.callback( - Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), - Input('role-menu-perms-radio-all-none', 'checked'), + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), + Input('role-menu-perms-radio-all-none', 'checked'), State('role-menu-store', 'data'), prevent_initial_call=True ) @@ -164,15 +165,27 @@ def all_none_role_menu_mode(all_none, menu_info): @app.callback( - Output('role-menu-perms', 'checkStrictly'), + [Output('role-menu-perms', 'checkStrictly'), + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True)], Input('role-menu-perms-radio-parent-children', 'checked'), + State('current-role-menu-store', 'data'), prevent_initial_call=True ) -def change_role_menu_mode(parent_children): +def change_role_menu_mode(parent_children, current_role_menu): if parent_children: - return False + checked_menu = [] + for item in current_role_menu: + has_children = False + for other_item in current_role_menu: + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) + return [False, checked_menu] else: - return True + checked_menu = [str(item.get('menu_id')) for item in current_role_menu if item] or [] + return [True, checked_menu] @app.callback( @@ -186,6 +199,7 @@ def change_role_menu_mode(parent_children): Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), Output('role-menu-store', 'data'), + Output('current-role-menu-store', 'data'), Output('role-remark', 'value'), Output('api-check-token', 'data', allow_duplicate=True), Output('role-add', 'nClicks'), @@ -219,6 +233,7 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, None, tree_data[1], None, + None, {'timestamp': time.time()}, None, None, @@ -233,7 +248,15 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, role_info_res = get_role_detail_api(role_id=role_id) if role_info_res['code'] == 200: role_info = role_info_res['data'] - checked_menu = [str(item.get('menu_id')) for item in role_info.get('menu') if item] or [] + checked_menu = [] + for item in role_info.get('menu'): + has_children = False + for other_item in role_info.get('menu'): + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [ True, '编辑角色', @@ -245,6 +268,7 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, [], checked_menu, tree_data[1], + role_info.get('menu'), role_info.get('role').get('remark'), {'timestamp': time.time()}, None, @@ -253,9 +277,9 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, {'type': 'edit'} ] - return [dash.no_update] * 11 + [{'timestamp': time.time()}, None, None, None, None] + return [dash.no_update] * 12 + [{'timestamp': time.time()}, None, None, None, None] - return [dash.no_update] * 12 + [None, None, None, None] + return [dash.no_update] * 13 + [None, None, None, None] @app.callback( @@ -277,12 +301,14 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, State('role-role_sort', 'value'), State('role-status', 'value'), State('role-menu-perms', 'checkedKeys'), + State('role-menu-perms', 'halfCheckedKeys'), State('role-remark', 'value')], prevent_initial_call=True ) -def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_perms, remark): +def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_checked_keys, menu_half_checked_eys, remark): if confirm_trigger: if all([role_name, role_key, role_sort]): + menu_perms = menu_half_checked_eys + menu_checked_keys params_add = dict(role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else None, status=status, remark=remark) params_edit = dict(role_id=cur_role_info.get('role_id') if cur_role_info else None, role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else '', status=status, remark=remark) @@ -407,7 +433,7 @@ def role_delete_modal(delete_click, button_click, return dash.no_update return [ - f'是否确认删除角色编号为{role_ids}的用户?', + f'是否确认删除角色编号为{role_ids}的角色?', True, {'role_ids': role_ids} ] diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index ee733b0..99abb4f 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -45,11 +45,12 @@ def get_search_dept_tree(dept_input): [State('user-user_name-input', 'value'), State('user-phone_number-input', 'value'), State('user-status-select', 'value'), - State('user-create_time-range', 'value')], + State('user-create_time-range', 'value'), + State('user-button-perms-container', 'data')], prevent_initial_call=True ) def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, pagination, operations, - user_name, phone_number, status_select, create_time_range): + user_name, phone_number, status_select, create_time_range, button_perms): dept_id = None create_time_start = None create_time_end = None @@ -101,15 +102,15 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio { 'title': '修改', 'icon': 'antd-edit' - }, + } if 'system:user:edit' in button_perms else None, { 'title': '删除', 'icon': 'antd-delete' - }, + } if 'system:user:remove' in button_perms else None, { 'title': '重置密码', 'icon': 'antd-key' - } + } if 'system:user:resetPwd' in button_perms else None ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index d04f47e..6cbc206 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -11,6 +11,9 @@ def render_store_container(): dcc.Store(id='api-check-result-container'), # token存储容器 dcc.Store(id='token-container'), + # 菜单信息存储容器 + dcc.Store(id='menu-info-store-container'), + dcc.Store(id='menu-list-store-container'), # 菜单current_key存储容器 dcc.Store(id='current-key-container'), # 用户管理模块操作类型存储容器 @@ -28,6 +31,7 @@ def render_store_container(): dcc.Store(id='role-delete-ids-store'), # 角色管理模块菜单权限存储容器 dcc.Store(id='role-menu-store'), + dcc.Store(id='current-role-menu-store'), # 菜单管理模块操作类型存储容器 dcc.Store(id='menu-operations-store'), dcc.Store(id='menu-operations-store-bk'), diff --git a/dash-fastapi-frontend/utils/tree_tool.py b/dash-fastapi-frontend/utils/tree_tool.py index 92f7188..c7f5792 100644 --- a/dash-fastapi-frontend/utils/tree_tool.py +++ b/dash-fastapi-frontend/utils/tree_tool.py @@ -118,6 +118,56 @@ def find_parents(tree, target_key): return result[::-1] +def deal_user_menu_info(pid: int, permission_list: list): + """ + 工具方法:根据菜单信息生成树形嵌套数据 + :param pid: 菜单id + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + menu_list = [] + for permission in permission_list: + if permission['parent_id'] == pid: + children = deal_user_menu_info(permission['menu_id'], permission_list) + antd_menu_list_data = {} + if children and permission['menu_type'] == 'M': + antd_menu_list_data['component'] = 'SubMenu' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'] + } + antd_menu_list_data['children'] = children + elif children and permission['menu_type'] == 'C': + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'], + 'href': permission['path'], + 'modules': permission['component'] + } + antd_menu_list_data['button'] = children + elif permission['menu_type'] == 'F': + antd_menu_list_data['component'] = 'Button' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'] + } + else: + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'], + 'href': permission['path'], + } + menu_list.append(antd_menu_list_data) + + return menu_list + + def get_dept_tree(pid: int, permission_list: list): """ 工具方法:根据部门信息生成树形嵌套数据 diff --git a/dash-fastapi-frontend/views/system/config/__init__.py b/dash-fastapi-frontend/views/system/config/__init__.py index 64badb5..9cdfad2 100644 --- a/dash-fastapi-frontend/views/system/config/__init__.py +++ b/dash-fastapi-frontend/views/system/config/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是参数设置') diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index 07160fe..3a22623 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -1,4 +1,4 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.dept_c @@ -6,7 +6,7 @@ from api.dept import get_dept_list_api from utils.tree_tool import get_dept_tree -def render(): +def render(button_perms): table_data_new = [] default_expanded_row_keys = [] table_info = get_dept_list_api({}) @@ -27,21 +27,22 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:dept:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:dept:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:dept:remove' in button_perms else {}, ] table_data_new = get_dept_tree(0, table_data) return [ + dcc.Store(id='dept-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -49,69 +50,74 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-dept_name-input', - placeholder='请输入部门名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='部门名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='dept-status-select', - placeholder='部门状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='部门状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='dept-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-dept_name-input', + placeholder='请输入部门名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='部门名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dept-status-select', + placeholder='部门状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='部门状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dept-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dept-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='dept-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:dept:query' not in button_perms + ), ) ] ), @@ -120,19 +126,24 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dept-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='dept-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:dept:add' not in button_perms ), fac.AntdButton( [ diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index 46a618d..e0aa134 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是字典管理') diff --git a/dash-fastapi-frontend/views/system/menu/__init__.py b/dash-fastapi-frontend/views/system/menu/__init__.py index 17fb505..4837da6 100644 --- a/dash-fastapi-frontend/views/system/menu/__init__.py +++ b/dash-fastapi-frontend/views/system/menu/__init__.py @@ -1,4 +1,4 @@ -from dash import html +from dash import dcc, html import feffery_antd_components as fac from api.menu import get_menu_list_api @@ -7,7 +7,7 @@ from views.system.menu.components.icon_category import render_icon import callbacks.system_c.menu_c.menu_c -def render(): +def render(button_perms): table_data_new = [] table_info = get_menu_list_api({}) if table_info['code'] == 200: @@ -33,21 +33,22 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:menu:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:menu:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:menu:remove' in button_perms else {}, ] table_data_new = list_to_tree(table_data) return [ + dcc.Store(id='menu-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( diff --git a/dash-fastapi-frontend/views/system/notice/__init__.py b/dash-fastapi-frontend/views/system/notice/__init__.py index 4bcc439..00785db 100644 --- a/dash-fastapi-frontend/views/system/notice/__init__.py +++ b/dash-fastapi-frontend/views/system/notice/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是通知公告') diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py index 69dd206..5e010fa 100644 --- a/dash-fastapi-frontend/views/system/post/__init__.py +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -1,11 +1,11 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.post_c from api.post import get_post_list_api -def render(): +def render(button_perms): post_params = dict(page_num=1, page_size=10) table_info = get_post_list_api(post_params) @@ -29,15 +29,16 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:post:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:post:remove' in button_perms else {}, ] return [ + dcc.Store(id='post-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -45,81 +46,86 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='post-post_code-input', - placeholder='请输入岗位编码', - autoComplete='off', - allowClear=True, - style={ - 'width': 210 - } - ), - label='岗位编码' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-post_name-input', - placeholder='请输入岗位名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 210 - } - ), - label='岗位名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='post-status-select', - placeholder='岗位状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 200 - } - ), - label='岗位状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='post-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-post_code-input', + placeholder='请输入岗位编码', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位编码' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-post_name-input', + placeholder='请输入岗位名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='post-status-select', + placeholder='岗位状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 200 + } + ), + label='岗位状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='post-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='post-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='post-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:post:query' not in button_perms + ), ) ] ), @@ -128,63 +134,83 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='post-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='post-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:post:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='post-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='post-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:post:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='post-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='post-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:post:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='post-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='post-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:post:export' not in button_perms ), ], style={ diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index ec7d1f9..e17ac94 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -5,7 +5,7 @@ import callbacks.system_c.role_c from api.role import get_role_list_api -def render(): +def render(button_perms): role_params = dict(page_num=1, page_size=10) table_info = get_role_list_api(role_params) @@ -29,15 +29,16 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:role:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:role:remove' in button_perms else {}, ] return [ + dcc.Store(id='role-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -45,89 +46,94 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdFormItem( - fac.AntdInput( - id='role-role_name-input', - placeholder='请输入角色名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 220 - } - ), - label='角色名称', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='role-role_key-input', - placeholder='请输入权限字符', - autoComplete='off', - allowClear=True, - style={ - 'width': 220 - } - ), - label='权限字符', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='role-status-select', - placeholder='角色状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 220 - } - ), - label='状态', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdDateRangePicker( - id='role-create_time-range', - style={ - 'width': 240 - } - ), - label='创建时间', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='role-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' - ) - ), - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='role-reset', - icon=fac.AntdIcon( - icon='antd-sync' + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='role-role_name-input', + placeholder='请输入角色名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='角色名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='role-role_key-input', + placeholder='请输入权限字符', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='权限字符', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='role-status-select', + placeholder='角色状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 220 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='role-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='role-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='role-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, ) - ), - style={ 'paddingBottom': '10px' }, + ], + layout='inline', ) ], - layout='inline', - ) + hidden='system:role:query' not in button_perms + ), ) ] ), @@ -136,63 +142,83 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='role-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='role-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:role:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='role-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='role-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:role:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='role-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='role-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:role:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='role-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='role-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:role:export' not in button_perms ), ], style={ diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 3718045..e067f9f 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -1,4 +1,4 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.user_c @@ -6,7 +6,7 @@ from api.user import get_user_list_api from api.dept import get_dept_tree_api -def render(): +def render(button_perms): dept_params = dict(dept_name='') user_params = dict(page_num=1, page_size=10) tree_info = get_dept_tree_api(dept_params) @@ -33,18 +33,19 @@ def render(): { 'title': '修改', 'icon': 'antd-edit' - }, + } if 'system:user:edit' in button_perms else None, { 'title': '删除', 'icon': 'antd-delete' - }, + } if 'system:user:remove' in button_perms else None, { 'title': '重置密码', 'icon': 'antd-key' - } + } if 'system:user:resetPwd' in button_perms else None ] return [ + dcc.Store(id='user-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -78,97 +79,102 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='user-user_name-input', - placeholder='请输入用户名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='用户名称' - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-phone_number-input', - placeholder='请输入手机号码', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='手机号码' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-status-select', - placeholder='用户状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='用户状态' - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdDateRangePicker( - id='user-create_time-range', - style={ - 'width': 240 - } - ), - label='创建时间' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='用户名称' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-phone_number-input', + placeholder='请输入手机号码', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='手机号码' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-status-select', + placeholder='用户状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='用户状态' + ), + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='user-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='user-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='user-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='user-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='user-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:user:query' not in button_perms + ), ) ] ), @@ -177,77 +183,102 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='user-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='user-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:user:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='user-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='user-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:user:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='user-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='user-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:user:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-up' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-up' + ), + '导入', + ], + id='user-import', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } ), - '导入', ], - id='user-import', - style={ - 'color': '#909399', - 'background': '#f4f4f5', - 'border-color': '#d3d4d6' - } + hidden='system:user:export' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='user-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='user-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:user:import' not in button_perms ), ], style={ -- Gitee From d26fd55a05f3039589b6f8a159b4a23ace9bed0e Mon Sep 17 00:00:00 2001 From: xlf Date: Tue, 11 Jul 2023 15:35:54 +0800 Subject: [PATCH 03/54] =?UTF-8?q?feat:=E5=90=8E=E7=AB=AF=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C=E6=8E=A5=E5=8F=A3=E6=9D=83=E9=99=90=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=B3=A8=E5=85=A5=EF=BC=8C=E6=94=AF=E6=8C=81=E5=AF=B9?= =?UTF-8?q?=E6=89=80=E6=9C=89=E6=8E=A5=E5=8F=A3=E8=BF=9B=E8=A1=8C=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/aspect/interface_auth.py | 19 +++++++++++++++++++ .../controller/dept_controller.py | 15 ++++++++------- .../controller/login_controller.py | 5 +++-- .../controller/menu_controller.py | 15 ++++++++------- .../module_admin/controller/post_controler.py | 13 +++++++------ .../controller/role_controller.py | 13 +++++++------ .../controller/user_controller.py | 11 ++++++----- dash-fastapi-frontend/callbacks/app_c.py | 2 +- .../views/monitor/operlog/__init__.py | 2 +- 9 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/aspect/interface_auth.py diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py new file mode 100644 index 0000000..ca25cc5 --- /dev/null +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -0,0 +1,19 @@ +from fastapi import Depends +from module_admin.entity.vo.user_vo import * +from module_admin.service.login_service import get_current_user +from module_admin.utils.response_util import AuthException + + +class CheckUserInterfaceAuth: + """ + 校验当前用户是否具有相应的接口权限 + """ + def __init__(self, perm_str: str = 'common'): + self.perm_str = perm_str + + def __call__(self, current_user: CurrentUserInfoServiceResponse = Depends(get_current_user)): + user_auth_list = [item.perms for item in current_user.menu] + user_auth_list.append('common') + if self.perm_str in user_auth_list: + return True + raise AuthException(data="", message="该用户无此接口权限") diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 186d2bb..217d78d 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.dept_vo import * from module_admin.dao.dept_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth deptController = APIRouter(dependencies=[Depends(get_current_user)]) -@deptController.post("/dept/tree", response_model=DeptTree) +@deptController.post("/dept/tree", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_services(query_db, dept_query) @@ -23,7 +24,7 @@ async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@deptController.post("/dept/forEditOption", response_model=DeptTree) +@deptController.post("/dept/forEditOption", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) @@ -34,7 +35,7 @@ async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: return response_500(data="", message="接口异常") -@deptController.post("/dept/get", response_model=DeptResponse) +@deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) @@ -45,7 +46,7 @@ async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@deptController.post("/dept/add", response_model=CrudDeptResponse) +@deptController.post("/dept/add", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:add'))]) async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -62,7 +63,7 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional return response_500(data="", message="接口异常") -@deptController.patch("/dept/edit", response_model=CrudDeptResponse) +@deptController.patch("/dept/edit", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -80,7 +81,7 @@ async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Option return response_500(data="", message="接口异常") -@deptController.post("/dept/delete", response_model=CrudDeptResponse) +@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:delete'))]) async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -98,7 +99,7 @@ async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, tok return response_500(data="", message="接口异常") -@deptController.get("/dept/{dept_id}", response_model=DeptModel) +@deptController.get("/dept/{dept_id}", response_model=DeptModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) async def query_detail_system_dept(dept_id: int, query_db: Session = Depends(get_db)): try: detail_dept_result = detail_dept_services(query_db, dept_id) diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 7d10809..378b19b 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -6,6 +6,7 @@ from module_admin.dao.login_dao import * from config.env import JwtConfig from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from datetime import timedelta @@ -45,7 +46,7 @@ async def login(request: Request, user: UserLogin, query_db: Session = Depends(g return response_500(data="", message="接口异常") -@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse, dependencies=[Depends(get_current_user)]) +@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse, dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) async def get_login_user_info(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -56,7 +57,7 @@ async def get_login_user_info(request: Request, token: Optional[str] = Header(.. return response_500(data="", message="接口异常") -@loginController.post("/logout", dependencies=[Depends(get_current_user)]) +@loginController.post("/logout", dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 4dc107c..b364cbc 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth menuController = APIRouter(dependencies=[Depends(get_current_user)]) -@menuController.post("/menu/tree", response_model=MenuTree) +@menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_services(query_db, menu_query) @@ -23,7 +24,7 @@ async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = De return response_500(data="", message="接口异常") -@menuController.post("/menu/forEditOption", response_model=MenuTree) +@menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) @@ -34,7 +35,7 @@ async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: return response_500(data="", message="接口异常") -@menuController.post("/menu/get", response_model=MenuResponse) +@menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) @@ -45,7 +46,7 @@ async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@menuController.post("/menu/add", response_model=CrudMenuResponse) +@menuController.post("/menu/add", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:add'))]) async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -62,7 +63,7 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional return response_500(data="", message="接口异常") -@menuController.patch("/menu/edit", response_model=CrudMenuResponse) +@menuController.patch("/menu/edit", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -80,7 +81,7 @@ async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Option return response_500(data="", message="接口异常") -@menuController.post("/menu/delete", response_model=CrudMenuResponse) +@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:delete'))]) async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): try: delete_menu_result = delete_menu_services(query_db, delete_menu) @@ -95,7 +96,7 @@ async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = D return response_500(data="", message="接口异常") -@menuController.get("/menu/{menu_id}", response_model=MenuModel) +@menuController.get("/menu/{menu_id}", response_model=MenuModel, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) async def query_detail_system_menu(menu_id: int, query_db: Session = Depends(get_db)): try: detail_menu_result = detail_menu_services(query_db, menu_id) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index f2fac53..edd0ec9 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -6,12 +6,13 @@ from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth postController = APIRouter(dependencies=[Depends(get_current_user)]) -@postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel) +@postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_post_select(query_db: Session = Depends(get_db)): try: role_query_result = get_post_select_option_services(query_db) @@ -22,7 +23,7 @@ async def get_system_post_select(query_db: Session = Depends(get_db)): return response_500(data="", message="接口异常") -@postController.post("/post/get", response_model=PostPageObjectResponse) +@postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) async def get_system_post_list(post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) @@ -33,7 +34,7 @@ async def get_system_post_list(post_query: PostPageObject, query_db: Session = D return response_500(data="", message="接口异常") -@postController.post("/post/add", response_model=CrudPostResponse) +@postController.post("/post/add", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:add'))]) async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -50,7 +51,7 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional return response_500(data="", message="接口异常") -@postController.patch("/post/edit", response_model=CrudPostResponse) +@postController.patch("/post/edit", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -68,7 +69,7 @@ async def edit_system_post(request: Request, edit_post: PostModel, token: Option return response_500(data="", message="接口异常") -@postController.post("/post/delete", response_model=CrudPostResponse) +@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:delete'))]) async def delete_system_post(delete_post: DeletePostModel, query_db: Session = Depends(get_db)): try: delete_post_result = delete_post_services(query_db, delete_post) @@ -83,7 +84,7 @@ async def delete_system_post(delete_post: DeletePostModel, query_db: Session = D return response_500(data="", message="接口异常") -@postController.get("/post/{post_id}", response_model=PostModel) +@postController.get("/post/{post_id}", response_model=PostModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) async def query_detail_system_post(post_id: int, query_db: Session = Depends(get_db)): try: detail_post_result = detail_post_services(query_db, post_id) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index fbcac03..3b8ef89 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -6,12 +6,13 @@ from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth roleController = APIRouter(dependencies=[Depends(get_current_user)]) -@roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel) +@roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_role_select(query_db: Session = Depends(get_db)): try: role_query_result = get_role_select_option_services(query_db) @@ -22,7 +23,7 @@ async def get_system_role_select(query_db: Session = Depends(get_db)): return response_500(data="", message="接口异常") -@roleController.post("/role/get", response_model=RolePageObjectResponse) +@roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) async def get_system_role_list(role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) @@ -33,7 +34,7 @@ async def get_system_role_list(role_query: RolePageObject, query_db: Session = D return response_500(data="", message="接口异常") -@roleController.post("/role/add", response_model=CrudRoleResponse) +@roleController.post("/role/add", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) async def add_system_role(request: Request, add_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -50,7 +51,7 @@ async def add_system_role(request: Request, add_role: AddRoleModel, token: Optio return response_500(data="", message="接口异常") -@roleController.patch("/role/edit", response_model=CrudRoleResponse) +@roleController.patch("/role/edit", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -68,7 +69,7 @@ async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Opt return response_500(data="", message="接口异常") -@roleController.post("/role/delete", response_model=CrudRoleResponse) +@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:delete'))]) async def delete_system_role(request: Request, delete_role: DeleteRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -86,7 +87,7 @@ async def delete_system_role(request: Request, delete_role: DeleteRoleModel, tok return response_500(data="", message="接口异常") -@roleController.get("/role/{role_id}", response_model=RoleDetailModel) +@roleController.get("/role/{role_id}", response_model=RoleDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) async def query_detail_system_role(role_id: int, query_db: Session = Depends(get_db)): try: delete_role_result = detail_role_services(query_db, role_id) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 2bfb939..68457f1 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth userController = APIRouter(dependencies=[Depends(get_current_user)]) -@userController.post("/user/get", response_model=UserPageObjectResponse) +@userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) async def get_system_user_list(user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) @@ -23,7 +24,7 @@ async def get_system_user_list(user_query: UserPageObject, query_db: Session = D return response_500(data="", message="接口异常") -@userController.post("/user/add", response_model=CrudUserResponse) +@userController.post("/user/add", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:add'))]) async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -41,7 +42,7 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio return response_500(data="", message="接口异常") -@userController.patch("/user/edit", response_model=CrudUserResponse) +@userController.patch("/user/edit", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -59,7 +60,7 @@ async def edit_system_user(request: Request, edit_user: AddUserModel, token: Opt return response_500(data="", message="接口异常") -@userController.post("/user/delete", response_model=CrudUserResponse) +@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:delete'))]) async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -77,7 +78,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.get("/user/{user_id}", response_model=UserDetailModel) +@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) diff --git a/dash-fastapi-frontend/callbacks/app_c.py b/dash-fastapi-frontend/callbacks/app_c.py index a8e8363..83b0041 100644 --- a/dash-fastapi-frontend/callbacks/app_c.py +++ b/dash-fastapi-frontend/callbacks/app_c.py @@ -16,7 +16,7 @@ from server import app, logger ) def check_api_response(data): - if session.get('code') == 401: + if session.get('code') == 401 and 'token' in session.get('message'): return [True, fuc.FefferyFancyNotification(session.get('message'), type='error', autoClose=2000)] elif session.get('code') == 200: diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index 5766ac5..b9b2243 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是操作日志') -- Gitee From 297dae88b22f59d1ab407d4621e33117136b2b95 Mon Sep 17 00:00:00 2001 From: xlf Date: Tue, 11 Jul 2023 17:06:27 +0800 Subject: [PATCH 04/54] =?UTF-8?q?refactor:=E8=B0=83=E6=95=B4utils=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 +- dash-fastapi-backend/module_admin/aspect/interface_auth.py | 2 +- .../module_admin/controller/dept_controller.py | 4 ++-- .../module_admin/controller/login_controller.py | 4 ++-- .../module_admin/controller/menu_controller.py | 4 ++-- .../module_admin/controller/post_controler.py | 4 ++-- .../module_admin/controller/role_controller.py | 4 ++-- .../module_admin/controller/user_controller.py | 4 ++-- dash-fastapi-backend/module_admin/dao/dept_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/login_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/menu_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/post_dao.py | 4 ++-- dash-fastapi-backend/module_admin/dao/role_dao.py | 4 ++-- dash-fastapi-backend/module_admin/dao/user_dao.py | 4 ++-- dash-fastapi-backend/module_admin/service/login_service.py | 4 ++-- dash-fastapi-backend/{module_admin => }/utils/log_util.py | 0 dash-fastapi-backend/{module_admin => }/utils/page_util.py | 0 .../{module_admin => }/utils/response_util.py | 0 .../{module_admin => }/utils/time_format_util.py | 0 dash-fastapi-frontend/views/monitor/logininfor/__init__.py | 2 +- 20 files changed, 26 insertions(+), 26 deletions(-) rename dash-fastapi-backend/{module_admin => }/utils/log_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/page_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/response_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/time_format_util.py (100%) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 9ceb84f..92958f1 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -12,7 +12,7 @@ from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from config.env import RedisConfig -from module_admin.utils.response_util import response_401, AuthException +from utils.response_util import response_401, AuthException app = FastAPI() diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py index ca25cc5..0100ee5 100644 --- a/dash-fastapi-backend/module_admin/aspect/interface_auth.py +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -1,7 +1,7 @@ from fastapi import Depends from module_admin.entity.vo.user_vo import * from module_admin.service.login_service import get_current_user -from module_admin.utils.response_util import AuthException +from utils.response_util import AuthException class CheckUserInterfaceAuth: diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 217d78d..40d90e2 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user from module_admin.service.dept_service import * from module_admin.entity.vo.dept_vo import * from module_admin.dao.dept_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 378b19b..d3a50dd 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -4,8 +4,8 @@ from module_admin.service.login_service import * from module_admin.entity.vo.login_vo import * from module_admin.dao.login_dao import * from config.env import JwtConfig -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from datetime import timedelta diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index b364cbc..40a6df2 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user from module_admin.service.menu_service import * from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index edd0ec9..5939291 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -4,8 +4,8 @@ from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 3b8ef89..1127a3e 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -4,8 +4,8 @@ from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 68457f1..fabea12 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user, get_password_ha from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/dao/dept_dao.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py index f4815d3..28c6f65 100644 --- a/dash-fastapi-backend/module_admin/dao/dept_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dept_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.dept_do import SysDept from module_admin.entity.vo.dept_vo import DeptModel, DeptResponse, CrudDeptResponse -from module_admin.utils.time_format_util import list_format_datetime +from utils.time_format_util import list_format_datetime def get_dept_by_id(db: Session, dept_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/login_dao.py b/dash-fastapi-backend/module_admin/dao/login_dao.py index e723c2e..710233a 100644 --- a/dash-fastapi-backend/module_admin/dao/login_dao.py +++ b/dash-fastapi-backend/module_admin/dao/login_dao.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from module_admin.entity.do.user_do import SysUser -from module_admin.utils.time_format_util import object_format_datetime +from utils.time_format_util import object_format_datetime def login_by_account(db: Session, user_name: str): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 421fbd5..63dbefa 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse -from module_admin.utils.time_format_util import list_format_datetime +from utils.time_format_util import list_format_datetime def get_menu_detail_by_id(db: Session, menu_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index 4b3a841..a9a70b1 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session from module_admin.entity.do.post_do import SysPost from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse -from module_admin.utils.time_format_util import list_format_datetime -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 45d5edb..6f4f38b 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import Session from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel -from module_admin.utils.time_format_util import list_format_datetime, object_format_datetime -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime, object_format_datetime +from utils.page_util import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index efc9682..158da3b 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -7,8 +7,8 @@ from module_admin.entity.do.post_do import SysPost from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ UserPageObjectResponse, CrudUserResponse -from module_admin.utils.time_format_util import list_format_datetime, format_datetime_dict_list -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime, format_datetime_dict_list +from utils.page_util import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index 49be7ca..7166dd5 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -4,8 +4,8 @@ from module_admin.dao.user_dao import * from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from datetime import datetime, timedelta from fastapi import Request from fastapi import Depends, Header diff --git a/dash-fastapi-backend/module_admin/utils/log_util.py b/dash-fastapi-backend/utils/log_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/log_util.py rename to dash-fastapi-backend/utils/log_util.py diff --git a/dash-fastapi-backend/module_admin/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/page_util.py rename to dash-fastapi-backend/utils/page_util.py diff --git a/dash-fastapi-backend/module_admin/utils/response_util.py b/dash-fastapi-backend/utils/response_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/response_util.py rename to dash-fastapi-backend/utils/response_util.py diff --git a/dash-fastapi-backend/module_admin/utils/time_format_util.py b/dash-fastapi-backend/utils/time_format_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/time_format_util.py rename to dash-fastapi-backend/utils/time_format_util.py diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py index 7734a12..c3e2dbb 100644 --- a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是登录日志') -- Gitee From 07975af2ab072e450eae98fcaa97f28a64cc7690 Mon Sep 17 00:00:00 2001 From: xlf Date: Wed, 12 Jul 2023 16:47:10 +0800 Subject: [PATCH 05/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E6=8E=A5=E5=8F=A3=E6=97=A5=E5=BF=97=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=EF=BC=9B=E6=96=B0=E5=A2=9E=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=88=E5=90=8E=E7=AB=AF=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../module_admin/annotation/log_annotation.py | 128 ++++++++++++++ .../module_admin/aspect/interface_auth.py | 2 +- .../controller/dept_controller.py | 15 +- .../module_admin/controller/log_controller.py | 80 +++++++++ .../controller/login_controller.py | 2 + .../controller/menu_controller.py | 17 +- .../module_admin/controller/post_controler.py | 15 +- .../controller/role_controller.py | 13 +- .../controller/user_controller.py | 11 +- .../module_admin/dao/log_dao.py | 161 ++++++++++++++++++ .../module_admin/entity/vo/log_vo.py | 110 ++++++++++++ .../module_admin/entity/vo/login_vo.py | 1 - .../module_admin/service/log_service.py | 98 +++++++++++ dash-fastapi-backend/utils/response_util.py | 6 +- dash-fastapi-frontend/callbacks/login_c.py | 2 +- dash-fastapi-frontend/utils/request.py | 6 +- 17 files changed, 638 insertions(+), 31 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/annotation/log_annotation.py create mode 100644 dash-fastapi-backend/module_admin/controller/log_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/log_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/log_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/log_service.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 92958f1..613d269 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -11,6 +11,7 @@ from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController +from module_admin.controller.log_controller import logController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -75,6 +76,7 @@ app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) +app.include_router(logController, prefix="/system", tags=['system/log']) if __name__ == '__main__': diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py new file mode 100644 index 0000000..c67cd57 --- /dev/null +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -0,0 +1,128 @@ +from functools import wraps +from fastapi import Request +import inspect +import os +import json +import time +from datetime import datetime +import requests +from user_agents import parse +from typing import Optional +from module_admin.service.login_service import get_current_user +from module_admin.service.log_service import add_operation_log_services, add_login_log_services +from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel + + +def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'operation'): + """ + 日志装饰器 + :param log_type: 日志类型(login表示登录日志,为空表示为操作日志) + :param title: 当前日志装饰器装饰的模块标题 + :param business_type: 业务类型(0其它 1新增 2修改 3删除 4授权 5导出 6导入 7强退 8生成代码 9清空数据) + :return: + """ + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.time() + # 获取被装饰函数的文件路径 + file_path = inspect.getfile(func) + # 获取项目根路径 + project_root = os.getcwd() + # 处理文件路径,去除项目根路径部分 + relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') + # 获取当前被装饰函数所在路径 + func_path = f'{relative_path}{func.__name__}' + # 获取上下文信息 + request: Request = kwargs.get('request') + token = request.headers.get('token') + query_db = kwargs.get('query_db') + request_method = request.method + operator_type = 0 + user_agent = request.headers.get('User-Agent') + if "Windows" in user_agent or "Macintosh" in user_agent or "Linux" in user_agent: + operator_type = 1 + if "Mobile" in user_agent or "Android" in user_agent or "iPhone" in user_agent: + operator_type = 2 + oper_url = request.url.path + oper_ip = request.headers.get('remote_addr') + oper_location = '内网IP' + try: + if oper_ip != '127.0.0.1' and oper_ip != 'localhost': + ip_result = requests.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}') + if ip_result.status_code == 200: + prov = ip_result.json().get('data').get('prov') + city = ip_result.json().get('data').get('city') + if prov or city: + oper_location = f'{prov}-{city}' + else: + oper_location = '未知' + else: + oper_location = '未知' + except Exception as e: + oper_location = '未知' + print(e) + finally: + payload = await request.body() + oper_param = json.dumps(json.loads(str(payload, 'utf-8')), ensure_ascii=False) + + # 调用原始函数 + oper_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + result = await func(*args, **kwargs) + cost_time = float(time.time() - start_time) * 100 + result_dict = json.loads(str(result.body, 'utf-8')) + json_result = json.dumps(dict(code=result_dict.get('code'), message=result_dict.get('message')), ensure_ascii=False) + status = 1 + error_msg = '' + if result_dict.get('code') == 200: + status = 0 + else: + error_msg = result_dict.get('message') + if log_type == 'login': + print(request.headers) + # user_agent_info = parse(user_agent) + # browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' + # system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' + # user = kwargs.get('user') + # user_name = user.user_name + # login_log = dict( + # user_name=user_name, + # ipaddr=oper_ip, + # login_location=oper_location, + # browser=browser, + # os=system_os, + # status=str(status), + # msg=result_dict.get('message'), + # login_time=oper_time + # ) + # + # add_login_log_services(query_db, LogininforModel(**login_log)) + else: + current_user = await get_current_user(request, token, query_db) + oper_name = current_user.user.user_name + dept_name = current_user.dept.dept_name + operation_log = dict( + title=title, + business_type=business_type, + method=func_path, + request_method=request_method, + operator_type=operator_type, + oper_name=oper_name, + dept_name=dept_name, + oper_url=oper_url, + oper_ip=oper_ip, + oper_location=oper_location, + oper_param=oper_param, + json_result=json_result, + status=status, + error_msg=error_msg, + oper_time=oper_time, + cost_time=cost_time + ) + add_operation_log_services(query_db, OperLogModel(**operation_log)) + + return result + + return wrapper + + return decorator diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py index 0100ee5..ebaaf14 100644 --- a/dash-fastapi-backend/module_admin/aspect/interface_auth.py +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -1,5 +1,5 @@ from fastapi import Depends -from module_admin.entity.vo.user_vo import * +from module_admin.entity.vo.user_vo import CurrentUserInfoServiceResponse from module_admin.service.login_service import get_current_user from utils.response_util import AuthException diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 40d90e2..2a11c24 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -8,13 +8,14 @@ from module_admin.dao.dept_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator deptController = APIRouter(dependencies=[Depends(get_current_user)]) @deptController.post("/dept/tree", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depends(get_db)): +async def get_system_dept_tree(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_services(query_db, dept_query) logger.info('获取成功') @@ -25,7 +26,7 @@ async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depend @deptController.post("/dept/forEditOption", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: Session = Depends(get_db)): +async def get_system_dept_tree_for_edit_option(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) logger.info('获取成功') @@ -36,7 +37,8 @@ async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: @deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) -async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depends(get_db)): +@log_decorator(title='部门管理', business_type=0) +async def get_system_dept_list(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) logger.info('获取成功') @@ -47,6 +49,7 @@ async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depend @deptController.post("/dept/add", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:add'))]) +@log_decorator(title='部门管理', business_type=1) async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -64,6 +67,7 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional @deptController.patch("/dept/edit", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) +@log_decorator(title='部门管理', business_type=2) async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -81,7 +85,8 @@ async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Option return response_500(data="", message="接口异常") -@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:delete'))]) +@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:remove'))]) +@log_decorator(title='部门管理', business_type=3) async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -100,7 +105,7 @@ async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, tok @deptController.get("/dept/{dept_id}", response_model=DeptModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) -async def query_detail_system_dept(dept_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_dept(request: Request, dept_id: int, query_db: Session = Depends(get_db)): try: detail_dept_result = detail_dept_services(query_db, dept_id) logger.info(f'获取dept_id为{dept_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py new file mode 100644 index 0000000..602e7e4 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -0,0 +1,80 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.log_service import * +from module_admin.entity.vo.log_vo import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user)]) + + +@logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) +@log_decorator(title='操作日志管理', business_type=0) +async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): + try: + operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) + logger.info('获取成功') + return response_200(data=operation_log_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/operation/delete", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@log_decorator(title='操作日志管理', business_type=3) +async def delete_system_operation_log(request: Request, delete_operation_log: DeleteOperLogModel, query_db: Session = Depends(get_db)): + try: + delete_operation_log_result = delete_operation_log_services(query_db, delete_operation_log) + if delete_operation_log_result.is_success: + logger.info(delete_operation_log_result.message) + return response_200(data=delete_operation_log_result, message=delete_operation_log_result.message) + else: + logger.warning(delete_operation_log_result.message) + return response_400(data="", message=delete_operation_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.get("/operation/{oper_id}", response_model=OperLogModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:query'))]) +async def query_detail_system_operation_log(request: Request, oper_id: int, query_db: Session = Depends(get_db)): + try: + detail_operation_log_result = detail_operation_log_services(query_db, oper_id) + logger.info(f'获取oper_id为{oper_id}的信息成功') + return response_200(data=detail_operation_log_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) +@log_decorator(title='登录日志管理', business_type=0) +async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): + try: + login_log_query_result = get_login_log_list_services(query_db, login_log_query) + logger.info('获取成功') + return response_200(data=login_log_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/login/delete", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) +@log_decorator(title='登录日志管理', business_type=3) +async def delete_system_login_log(request: Request, delete_login_log: DeleteLoginLogModel, query_db: Session = Depends(get_db)): + try: + delete_login_log_result = delete_login_log_services(query_db, delete_login_log) + if delete_login_log_result.is_success: + logger.info(delete_login_log_result.message) + return response_200(data=delete_login_log_result, message=delete_login_log_result.message) + else: + logger.warning(delete_login_log_result.message) + return response_400(data="", message=delete_login_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index d3a50dd..6876ba4 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -7,6 +7,7 @@ from config.env import JwtConfig from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator from datetime import timedelta @@ -14,6 +15,7 @@ loginController = APIRouter() @loginController.post("/loginByAccount", response_model=Token) +@log_decorator(title='用户登录', business_type=0, log_type='login') async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): try: result = authenticate_user(query_db, user.user_name, user.password) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 40a6df2..06579d5 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -8,13 +8,14 @@ from module_admin.dao.menu_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_services(query_db, menu_query) logger.info('获取成功') @@ -25,7 +26,7 @@ async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = De @menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) logger.info('获取成功') @@ -36,7 +37,8 @@ async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depends(get_db)): +@log_decorator(title='菜单管理', business_type=0) +async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) logger.info('获取成功') @@ -47,6 +49,7 @@ async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depend @menuController.post("/menu/add", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:add'))]) +@log_decorator(title='菜单管理', business_type=1) async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -64,6 +67,7 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional @menuController.patch("/menu/edit", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) +@log_decorator(title='菜单管理', business_type=2) async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -81,8 +85,9 @@ async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Option return response_500(data="", message="接口异常") -@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:delete'))]) -async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): +@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:remove'))]) +@log_decorator(title='菜单管理', business_type=3) +async def delete_system_menu(request: Request, delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): try: delete_menu_result = delete_menu_services(query_db, delete_menu) if delete_menu_result.is_success: @@ -97,7 +102,7 @@ async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = D @menuController.get("/menu/{menu_id}", response_model=MenuModel, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) -async def query_detail_system_menu(menu_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_menu(request: Request, menu_id: int, query_db: Session = Depends(get_db)): try: detail_menu_result = detail_menu_services(query_db, menu_id) logger.info(f'获取menu_id为{menu_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index 5939291..a11d4e9 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -7,13 +7,14 @@ from module_admin.entity.vo.post_vo import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator postController = APIRouter(dependencies=[Depends(get_current_user)]) @postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_post_select(query_db: Session = Depends(get_db)): +async def get_system_post_select(request: Request, query_db: Session = Depends(get_db)): try: role_query_result = get_post_select_option_services(query_db) logger.info('获取成功') @@ -24,7 +25,8 @@ async def get_system_post_select(query_db: Session = Depends(get_db)): @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -async def get_system_post_list(post_query: PostPageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='岗位管理', business_type=0) +async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) logger.info('获取成功') @@ -35,6 +37,7 @@ async def get_system_post_list(post_query: PostPageObject, query_db: Session = D @postController.post("/post/add", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:add'))]) +@log_decorator(title='岗位管理', business_type=1) async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -52,6 +55,7 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional @postController.patch("/post/edit", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) +@log_decorator(title='岗位管理', business_type=2) async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -69,8 +73,9 @@ async def edit_system_post(request: Request, edit_post: PostModel, token: Option return response_500(data="", message="接口异常") -@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:delete'))]) -async def delete_system_post(delete_post: DeletePostModel, query_db: Session = Depends(get_db)): +@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:remove'))]) +@log_decorator(title='岗位管理', business_type=3) +async def delete_system_post(request: Request, delete_post: DeletePostModel, query_db: Session = Depends(get_db)): try: delete_post_result = delete_post_services(query_db, delete_post) if delete_post_result.is_success: @@ -85,7 +90,7 @@ async def delete_system_post(delete_post: DeletePostModel, query_db: Session = D @postController.get("/post/{post_id}", response_model=PostModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) -async def query_detail_system_post(post_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_post(request: Request, post_id: int, query_db: Session = Depends(get_db)): try: detail_post_result = detail_post_services(query_db, post_id) logger.info(f'获取post_id为{post_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 1127a3e..c107855 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -7,13 +7,14 @@ from module_admin.entity.vo.role_vo import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator roleController = APIRouter(dependencies=[Depends(get_current_user)]) @roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_role_select(query_db: Session = Depends(get_db)): +async def get_system_role_select(request: Request, query_db: Session = Depends(get_db)): try: role_query_result = get_role_select_option_services(query_db) logger.info('获取成功') @@ -24,7 +25,8 @@ async def get_system_role_select(query_db: Session = Depends(get_db)): @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -async def get_system_role_list(role_query: RolePageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='角色管理', business_type=0) +async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) logger.info('获取成功') @@ -35,6 +37,7 @@ async def get_system_role_list(role_query: RolePageObject, query_db: Session = D @roleController.post("/role/add", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) +@log_decorator(title='角色管理', business_type=1) async def add_system_role(request: Request, add_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -52,6 +55,7 @@ async def add_system_role(request: Request, add_role: AddRoleModel, token: Optio @roleController.patch("/role/edit", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@log_decorator(title='角色管理', business_type=2) async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -69,7 +73,8 @@ async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Opt return response_500(data="", message="接口异常") -@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:delete'))]) +@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:remove'))]) +@log_decorator(title='角色管理', business_type=3) async def delete_system_role(request: Request, delete_role: DeleteRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -88,7 +93,7 @@ async def delete_system_role(request: Request, delete_role: DeleteRoleModel, tok @roleController.get("/role/{role_id}", response_model=RoleDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) -async def query_detail_system_role(role_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_role(request: Request, role_id: int, query_db: Session = Depends(get_db)): try: delete_role_result = detail_role_services(query_db, role_id) logger.info(f'获取role_id为{role_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index fabea12..e58b7a3 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -8,13 +8,15 @@ from module_admin.dao.user_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -async def get_system_user_list(user_query: UserPageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='用户管理', business_type=0) +async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) logger.info('获取成功') @@ -25,6 +27,7 @@ async def get_system_user_list(user_query: UserPageObject, query_db: Session = D @userController.post("/user/add", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:add'))]) +@log_decorator(title='用户管理', business_type=1) async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -43,6 +46,7 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio @userController.patch("/user/edit", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@log_decorator(title='用户管理', business_type=2) async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -60,7 +64,8 @@ async def edit_system_user(request: Request, edit_user: AddUserModel, token: Opt return response_500(data="", message="接口异常") -@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:delete'))]) +@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:remove'))]) +@log_decorator(title='用户管理', business_type=3) async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -79,7 +84,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok @userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) -async def query_detail_system_user(user_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) logger.info(f'获取user_id为{user_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py new file mode 100644 index 0000000..3c4a1d2 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -0,0 +1,161 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.log_do import SysOperLog, SysLogininfor +from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ + LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info +from datetime import datetime, time + + +def get_operation_log_detail_by_id(db: Session, oper_id: int): + operation_log_info = db.query(SysOperLog) \ + .filter(SysOperLog.oper_id == oper_id) \ + .first() + + return operation_log_info + + +def get_operation_log_list(db: Session, page_object: OperLogPageObject): + """ + 根据查询参数获取操作日志列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 操作日志列表信息对象 + """ + count = db.query(SysOperLog) \ + .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, + SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, + SysOperLog.business_type == page_object.business_type if page_object.business_type else True, + SysOperLog.status == page_object.status if page_object.status else True, + SysOperLog.oper_time.between( + datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.oper_time_start and page_object.oper_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + operation_log_list = db.query(SysOperLog) \ + .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, + SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, + SysOperLog.business_type == page_object.business_type if page_object.business_type else True, + SysOperLog.status == page_object.status if page_object.status else True, + SysOperLog.oper_time.between( + datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.oper_time_start and page_object.oper_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(operation_log_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return OperLogPageObjectResponse(**result) + + +def add_operation_log_dao(db: Session, operation_log: OperLogModel): + """ + 新增操作日志数据库操作 + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: 新增校验结果 + """ + db_operation_log = SysOperLog(**operation_log.dict()) + db.add(db_operation_log) + db.commit() # 提交保存到数据库中 + db.refresh(db_operation_log) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudLogResponse(**result) + + +def delete_operation_log_dao(db: Session, operation_log: OperLogModel): + """ + 删除操作日志数据库操作 + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: + """ + db.query(SysOperLog) \ + .filter(SysOperLog.oper_id == operation_log.oper_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def get_login_log_list(db: Session, page_object: LoginLogPageObject): + """ + 根据查询参数获取登录日志列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 登录日志列表信息对象 + """ + count = db.query(SysLogininfor) \ + .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysLogininfor.status == page_object.status if page_object.status else True, + SysLogininfor.login_time.between( + datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.login_time_start and page_object.login_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + login_log_list = db.query(SysLogininfor) \ + .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysLogininfor.status == page_object.status if page_object.status else True, + SysLogininfor.login_time.between( + datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.login_time_start and page_object.login_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(login_log_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return LoginLogPageObjectResponse(**result) + + +def add_login_log_dao(db: Session, login_log: LogininforModel): + """ + 新增登录日志数据库操作 + :param db: orm对象 + :param login_log: 登录日志对象 + :return: 新增校验结果 + """ + db_login_log = SysLogininfor(**login_log.dict()) + db.add(db_login_log) + db.commit() # 提交保存到数据库中 + db.refresh(db_login_log) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudLogResponse(**result) + + +def delete_login_log_dao(db: Session, login_log: LogininforModel): + """ + 删除登录日志数据库操作 + :param db: orm对象 + :param login_log: 登录日志对象 + :return: + """ + db.query(SysLogininfor) \ + .filter(SysLogininfor.info_id == login_log.info_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py new file mode 100644 index 0000000..2f87702 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -0,0 +1,110 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class OperLogModel(BaseModel): + """ + 操作日志表对应pydantic模型 + """ + oper_id: Optional[int] + title: Optional[str] + business_type: Optional[int] + method: Optional[str] + request_method: Optional[str] + operator_type: Optional[int] + oper_name: Optional[str] + dept_name: Optional[str] + oper_url: Optional[str] + oper_ip: Optional[str] + oper_location: Optional[str] + oper_param: Optional[str] + json_result: Optional[str] + status: Optional[int] + error_msg: Optional[str] + oper_time: Optional[str] + cost_time: Optional[int] + + class Config: + orm_mode = True + + +class LogininforModel(BaseModel): + """ + 登录日志表对应pydantic模型 + """ + info_id: Optional[int] + user_name: Optional[str] + ipaddr: Optional[str] + login_location: Optional[str] + browser: Optional[str] + os: Optional[str] + status: Optional[str] + msg: Optional[str] + login_time: Optional[str] + + class Config: + orm_mode = True + + +class OperLogPageObject(OperLogModel): + """ + 操作日志管理分页查询模型 + """ + oper_time_start: Optional[str] + oper_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class OperLogPageObjectResponse(BaseModel): + """ + 操作日志列表分页查询返回模型 + """ + rows: List[Union[OperLogModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteOperLogModel(BaseModel): + """ + 删除操作日志模型 + """ + oper_ids: str + + +class LoginLogPageObject(LogininforModel): + """ + 登录日志管理分页查询模型 + """ + login_time_start: Optional[str] + login_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class LoginLogPageObjectResponse(BaseModel): + """ + 登录日志列表分页查询返回模型 + """ + rows: List[Union[LogininforModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteLoginLogModel(BaseModel): + """ + 删除登录日志模型 + """ + info_ids: str + + +class CrudLogResponse(BaseModel): + """ + 操作各类日志响应模型 + """ + is_success: bool + message: str diff --git a/dash-fastapi-backend/module_admin/entity/vo/login_vo.py b/dash-fastapi-backend/module_admin/entity/vo/login_vo.py index d7b3056..fa08a4a 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/login_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/login_vo.py @@ -5,7 +5,6 @@ from typing import Optional class UserLogin(BaseModel): user_name: str password: str - user_request: Optional[str] = None class Token(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py new file mode 100644 index 0000000..23ac492 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -0,0 +1,98 @@ +from module_admin.entity.vo.log_vo import * +from module_admin.dao.log_dao import * + + +def get_operation_log_list_services(result_db: Session, page_object: OperLogPageObject): + """ + 获取操作日志列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 操作日志列表信息对象 + """ + operation_log_list_result = get_operation_log_list(result_db, page_object) + + return operation_log_list_result + + +def add_operation_log_services(result_db: Session, page_object: OperLogModel): + """ + 新增操作日志service + :param result_db: orm对象 + :param page_object: 新增操作日志对象 + :return: 新增操作日志校验结果 + """ + add_operation_log_result = add_operation_log_dao(result_db, page_object) + + return add_operation_log_result + + +def delete_operation_log_services(result_db: Session, page_object: DeleteOperLogModel): + """ + 删除操作日志信息service + :param result_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.oper_ids.split(','): + oper_id_list = page_object.oper_ids.split(',') + for oper_id in oper_id_list: + oper_id_dict = dict(oper_id=oper_id) + delete_operation_log_dao(result_db, OperLogModel(**oper_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入操作日志id为空') + return CrudLogResponse(**result) + + +def detail_operation_log_services(result_db: Session, oper_id: int): + """ + 获取操作日志详细信息service + :param result_db: orm对象 + :param oper_id: 操作日志id + :return: 操作日志id对应的信息 + """ + operation_log = get_operation_log_detail_by_id(result_db, oper_id=oper_id) + + return operation_log + + +def get_login_log_list_services(result_db: Session, page_object: LoginLogPageObject): + """ + 获取登录日志列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 登录日志列表信息对象 + """ + operation_log_list_result = get_login_log_list(result_db, page_object) + + return operation_log_list_result + + +def add_login_log_services(result_db: Session, page_object: LogininforModel): + """ + 新增登录日志service + :param result_db: orm对象 + :param page_object: 新增登录日志对象 + :return: 新增登录日志校验结果 + """ + add_login_log_result = add_login_log_dao(result_db, page_object) + + return add_login_log_result + + +def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogModel): + """ + 删除操作日志信息service + :param result_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') + return CrudLogResponse(**result) diff --git a/dash-fastapi-backend/utils/response_util.py b/dash-fastapi-backend/utils/response_util.py index 6140611..b5f7607 100644 --- a/dash-fastapi-backend/utils/response_util.py +++ b/dash-fastapi-backend/utils/response_util.py @@ -23,7 +23,7 @@ def response_200(*, data: Union[list, dict, str], message="获取成功") -> Res def response_400(*, data: str = None, message: str = "获取失败") -> Response: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content=( + content=jsonable_encoder( { 'code': 400, 'message': message, @@ -38,7 +38,7 @@ def response_400(*, data: str = None, message: str = "获取失败") -> Response def response_401(*, data: str = None, message: str = "获取失败") -> Response: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, - content=( + content=jsonable_encoder( { 'code': 401, 'message': message, @@ -53,7 +53,7 @@ def response_401(*, data: str = None, message: str = "获取失败") -> Response def response_500(*, data: str = None, message: str = "接口异常") -> Response: return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=( + content=jsonable_encoder( { 'code': 500, 'message': message, diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py index 6272036..7a35d07 100644 --- a/dash-fastapi-frontend/callbacks/login_c.py +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -35,7 +35,7 @@ def login_auth(nClicks, username, password, captcha, input_captcha): if captcha == input_captcha: try: - user_params = dict(user_name=username, password=password, user_request=str(request.headers)) + user_params = dict(user_name=username, password=password) userinfo_result = login_api(user_params) if userinfo_result['code'] == 200: token = userinfo_result['data']['token'] diff --git a/dash-fastapi-frontend/utils/request.py b/dash-fastapi-frontend/utils/request.py index cfc6a66..9654d91 100644 --- a/dash-fastapi-frontend/utils/request.py +++ b/dash-fastapi-frontend/utils/request.py @@ -9,9 +9,11 @@ def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] json: Optional[dict] = None, timeout: Optional[int] = None): api_url = ApiBaseUrlConfig.BaseUrl + url method = method.lower().strip() - api_headers = None + user_agent = request.headers.get('User-Agent') if is_headers: - api_headers = {'token': 'Bearer' + session.get('token')} + api_headers = {'token': 'Bearer' + session.get('token'), 'remote_addr': request.remote_addr, 'User-Agent': user_agent} + else: + api_headers = {'remote_addr': request.remote_addr, 'User-Agent': user_agent} try: if method == 'get': response = requests.get(url=api_url, params=params, data=data, json=json, headers=api_headers, -- Gitee From 27708d1900992965693c55dca03f4cf2c53c714e Mon Sep 17 00:00:00 2001 From: xlf Date: Wed, 12 Jul 2023 20:55:43 +0800 Subject: [PATCH 06/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/annotation/log_annotation.py | 38 +- .../controller/dept_controller.py | 1 - .../module_admin/controller/log_controller.py | 18 +- .../controller/menu_controller.py | 1 - .../module_admin/controller/post_controler.py | 1 - .../controller/role_controller.py | 1 - .../controller/user_controller.py | 1 - .../module_admin/dao/log_dao.py | 26 +- .../module_admin/entity/vo/log_vo.py | 7 + .../module_admin/service/log_service.py | 34 +- dash-fastapi-frontend/api/log.py | 31 + .../callbacks/monitor_c/operlog_c.py | 268 ++++++++ .../callbacks/system_c/post_c.py | 7 +- .../callbacks/system_c/role_c.py | 7 +- .../callbacks/system_c/user_c.py | 7 +- dash-fastapi-frontend/store/store.py | 4 + .../views/monitor/operlog/__init__.py | 595 +++++++++++++++++- 17 files changed, 1000 insertions(+), 47 deletions(-) create mode 100644 dash-fastapi-frontend/api/log.py create mode 100644 dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index c67cd57..ddeac6c 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -32,7 +32,7 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope # 处理文件路径,去除项目根路径部分 relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') # 获取当前被装饰函数所在路径 - func_path = f'{relative_path}{func.__name__}' + func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 request: Request = kwargs.get('request') token = request.headers.get('token') @@ -79,24 +79,24 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope else: error_msg = result_dict.get('message') if log_type == 'login': - print(request.headers) - # user_agent_info = parse(user_agent) - # browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' - # system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' - # user = kwargs.get('user') - # user_name = user.user_name - # login_log = dict( - # user_name=user_name, - # ipaddr=oper_ip, - # login_location=oper_location, - # browser=browser, - # os=system_os, - # status=str(status), - # msg=result_dict.get('message'), - # login_time=oper_time - # ) - # - # add_login_log_services(query_db, LogininforModel(**login_log)) + # print(request.headers) + user_agent_info = parse(user_agent) + browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' + system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' + user = kwargs.get('user') + user_name = user.user_name + login_log = dict( + user_name=user_name, + ipaddr=oper_ip, + login_location=oper_location, + browser=browser, + os=system_os, + status=str(status), + msg=result_dict.get('message'), + login_time=oper_time + ) + + add_login_log_services(query_db, LogininforModel(**login_log)) else: current_user = await get_current_user(request, token, query_db) oper_name = current_user.user.user_name diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 2a11c24..786d9e0 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -37,7 +37,6 @@ async def get_system_dept_tree_for_edit_option(request: Request, dept_query: Dep @deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) -@log_decorator(title='部门管理', business_type=0) async def get_system_dept_list(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index 602e7e4..b714327 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -14,7 +14,6 @@ logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user) @logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) -@log_decorator(title='操作日志管理', business_type=0) async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): try: operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) @@ -41,6 +40,22 @@ async def delete_system_operation_log(request: Request, delete_operation_log: De return response_500(data="", message="接口异常") +@logController.post("/operation/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@log_decorator(title='操作日志管理', business_type=9) +async def clear_system_operation_log(request: Request, clear_operation_log: ClearOperLogModel, query_db: Session = Depends(get_db)): + try: + clear_operation_log_result = clear_operation_log_services(query_db, clear_operation_log) + if clear_operation_log_result.is_success: + logger.info(clear_operation_log_result.message) + return response_200(data=clear_operation_log_result, message=clear_operation_log_result.message) + else: + logger.warning(clear_operation_log_result.message) + return response_400(data="", message=clear_operation_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + @logController.get("/operation/{oper_id}", response_model=OperLogModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:query'))]) async def query_detail_system_operation_log(request: Request, oper_id: int, query_db: Session = Depends(get_db)): try: @@ -53,7 +68,6 @@ async def query_detail_system_operation_log(request: Request, oper_id: int, quer @logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) -@log_decorator(title='登录日志管理', business_type=0) async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): try: login_log_query_result = get_login_log_list_services(query_db, login_log_query) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 06579d5..8703183 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -37,7 +37,6 @@ async def get_system_menu_tree_for_edit_option(request: Request, menu_query: Men @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -@log_decorator(title='菜单管理', business_type=0) async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index a11d4e9..740f9e8 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -25,7 +25,6 @@ async def get_system_post_select(request: Request, query_db: Session = Depends(g @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -@log_decorator(title='岗位管理', business_type=0) async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index c107855..562fff2 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -25,7 +25,6 @@ async def get_system_role_select(request: Request, query_db: Session = Depends(g @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -@log_decorator(title='角色管理', business_type=0) async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index e58b7a3..bfaafef 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -15,7 +15,6 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -@log_decorator(title='用户管理', business_type=0) async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py index 3c4a1d2..8447a15 100644 --- a/dash-fastapi-backend/module_admin/dao/log_dao.py +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.log_do import SysOperLog, SysLogininfor from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse -from utils.time_format_util import list_format_datetime +from utils.time_format_util import object_format_datetime, list_format_datetime from utils.page_util import get_page_info from datetime import datetime, time @@ -12,7 +12,7 @@ def get_operation_log_detail_by_id(db: Session, oper_id: int): .filter(SysOperLog.oper_id == oper_id) \ .first() - return operation_log_info + return object_format_datetime(operation_log_info) def get_operation_log_list(db: Session, page_object: OperLogPageObject): @@ -89,6 +89,17 @@ def delete_operation_log_dao(db: Session, operation_log: OperLogModel): db.commit() # 提交保存到数据库中 +def clear_operation_log_dao(db: Session): + """ + 清除操作日志数据库操作 + :param db: orm对象 + :return: + """ + db.query(SysOperLog) \ + .delete() + db.commit() # 提交保存到数据库中 + + def get_login_log_list(db: Session, page_object: LoginLogPageObject): """ 根据查询参数获取登录日志列表信息 @@ -159,3 +170,14 @@ def delete_login_log_dao(db: Session, login_log: LogininforModel): .filter(SysLogininfor.info_id == login_log.info_id) \ .delete() db.commit() # 提交保存到数据库中 + + +def clear_login_log_dao(db: Session): + """ + 清除登录日志数据库操作 + :param db: orm对象 + :return: + """ + db.query(SysLogininfor) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py index 2f87702..fb778b6 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -74,6 +74,13 @@ class DeleteOperLogModel(BaseModel): oper_ids: str +class ClearOperLogModel(BaseModel): + """ + 清除操作日志模型 + """ + oper_type: str + + class LoginLogPageObject(LogininforModel): """ 登录日志管理分页查询模型 diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 23ac492..06301cc 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -44,6 +44,22 @@ def delete_operation_log_services(result_db: Session, page_object: DeleteOperLog return CrudLogResponse(**result) +def clear_operation_log_services(result_db: Session, page_object: ClearOperLogModel): + """ + 清除操作日志信息service + :param result_db: orm对象 + :param page_object: 清除操作日志对象 + :return: 清除操作日志校验结果 + """ + if page_object.oper_type == 'clear': + clear_operation_log_dao(result_db) + result = dict(is_success=True, message='清除成功') + else: + result = dict(is_success=False, message='清除标识不合法') + + return CrudLogResponse(**result) + + def detail_operation_log_services(result_db: Session, oper_id: int): """ 获取操作日志详细信息service @@ -87,12 +103,16 @@ def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogMod :param page_object: 删除操作日志对象 :return: 删除操作日志校验结果 """ - if page_object.info_ids.split(','): - info_id_list = page_object.info_ids.split(',') - for info_id in info_id_list: - info_id_dict = dict(info_id=info_id) - delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) - result = dict(is_success=True, message='删除成功') + if page_object.oper_type == 'clear': + clear_operation_log_dao(result_db) + result = dict(is_success=True, message='清除成功') else: - result = dict(is_success=False, message='传入登录日志id为空') + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') return CrudLogResponse(**result) diff --git a/dash-fastapi-frontend/api/log.py b/dash-fastapi-frontend/api/log.py new file mode 100644 index 0000000..714af6d --- /dev/null +++ b/dash-fastapi-frontend/api/log.py @@ -0,0 +1,31 @@ +from utils.request import api_request + + +def get_operation_log_list_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/get', is_headers=True, json=page_obj) + + +def delete_operation_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/delete', is_headers=True, json=page_obj) + + +def clear_operation_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/clear', is_headers=True, json=page_obj) + + +def get_operation_log_detail_api(oper_id: int): + + return api_request(method='get', url=f'/system/log/operation/{oper_id}', is_headers=True) + + +def get_login_log_list_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/get', is_headers=True, json=page_obj) + + +def delete_login_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/delete', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py new file mode 100644 index 0000000..09dfc19 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -0,0 +1,268 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.log import get_operation_log_list_api, get_operation_log_detail_api, delete_operation_log_api, clear_operation_log_api + + +@app.callback( + [Output('operation_log-list-table', 'data', allow_duplicate=True), + Output('operation_log-list-table', 'pagination', allow_duplicate=True), + Output('operation_log-list-table', 'key'), + Output('operation_log-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('operation_log-search', 'nClicks'), + Input('operation_log-list-table', 'pagination'), + Input('operation_log-operations-store', 'data')], + [State('operation_log-title-input', 'value'), + State('operation_log-oper_name-input', 'value'), + State('operation_log-business_type-select', 'value'), + State('operation_log-status-select', 'value'), + State('operation_log-oper_time-range', 'value'), + State('operation_log-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_operation_log_table_data(search_click, pagination, operations, title, oper_name, business_type, status_select, oper_time_range, button_perms): + + oper_time_start = None + oper_time_end = None + if oper_time_range: + oper_time_start = oper_time_range[0] + oper_time_end = oper_time_range[1] + query_params = dict( + title=title, + oper_name=oper_name, + business_type=business_type, + status=status_select, + oper_time_start=oper_time_start, + oper_time_end=oper_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + title=title, + oper_name=oper_name, + business_type=business_type, + status=status_select, + oper_time_start=oper_time_start, + oper_time_end=oper_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_operation_log_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == 0: + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + if item['business_type'] == 0: + item['business_type'] = dict(tag='其他', color='purple') + elif item['business_type'] == 1: + item['business_type'] = dict(tag='新增', color='green') + elif item['business_type'] == 2: + item['business_type'] = dict(tag='修改', color='orange') + elif item['business_type'] == 3: + item['business_type'] = dict(tag='删除', color='red') + elif item['business_type'] == 4: + item['business_type'] = dict(tag='授权', color='lime') + elif item['business_type'] == 5: + item['business_type'] = dict(tag='导出', color='geekblue') + elif item['business_type'] == 6: + item['business_type'] = dict(tag='导入', color='blue') + elif item['business_type'] == 7: + item['business_type'] = dict(tag='强退', color='magenta') + elif item['business_type'] == 8: + item['business_type'] = dict(tag='生成代码', color='cyan') + elif item['business_type'] == 9: + item['business_type'] = dict(tag='清空数据', color='volcano') + item['key'] = str(item['oper_id']) + item['cost_time'] = f"{item['cost_time']}毫秒" + item['operation'] = [ + { + 'content': '详情', + 'type': 'link', + 'icon': 'antd-eye' + } if 'monitor:operlog:query' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('operation_log-title-input', 'value'), + Output('operation_log-oper_name-input', 'value'), + Output('operation_log-business_type-select', 'value'), + Output('operation_log-status-select', 'value'), + Output('operation_log-oper_time-range', 'value'), + Output('operation_log-operations-store', 'data')], + Input('operation_log-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_operation_log_query_params(reset_click): + if reset_click: + return [None, None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 6 + + +@app.callback( + [Output('operation_log-modal', 'visible', allow_duplicate=True), + Output('operation_log-modal', 'title'), + Output('operation_log-title-text', 'children'), + Output('operation_log-oper_url-text', 'children'), + Output('operation_log-login_info-text', 'children'), + Output('operation_log-request_method-text', 'children'), + Output('operation_log-method-text', 'children'), + Output('operation_log-oper_param-text', 'children'), + Output('operation_log-json_result-text', 'children'), + Output('operation_log-status-text', 'children'), + Output('operation_log-cost_time-text', 'children'), + Output('operation_log-oper_time-text', 'children'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('operation_log-list-table', 'nClicksButton'), + [State('operation_log-list-table', 'clickedContent'), + State('operation_log-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_edit_operation_log_modal(button_click, clicked_content, recently_button_clicked_row): + if button_click: + oper_id = int(recently_button_clicked_row['key']) + operation_log_info_res = get_operation_log_detail_api(oper_id=oper_id) + if operation_log_info_res['code'] == 200: + operation_log_info = operation_log_info_res['data'] + oper_name = operation_log_info.get('oper_name') if operation_log_info.get('oper_name') else '' + oper_ip = operation_log_info.get('oper_ip') if operation_log_info.get('oper_ip') else '' + oper_location = operation_log_info.get('oper_location') if operation_log_info.get('oper_location') else '' + login_info = f'{oper_name} / {oper_ip} / {oper_location}' + return [ + True, + '操作日志详情', + operation_log_info.get('title'), + operation_log_info.get('oper_url'), + login_info, + operation_log_info.get('request_method'), + operation_log_info.get('method'), + operation_log_info.get('oper_param'), + operation_log_info.get('json_result'), + '正常' if operation_log_info.get('status') == 0 else '失败', + f"{operation_log_info.get('cost_time')}毫秒", + operation_log_info.get('oper_time'), + {'timestamp': time.time()}, + ] + + return [dash.no_update] * 12 + [{'timestamp': time.time()}] + + return [dash.no_update] * 13 + + +@app.callback( + Output('operation_log-delete', 'disabled'), + Input('operation_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_operation_log_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return False + + return False + + return True + + +@app.callback( + [Output('operation_log-delete-text', 'children'), + Output('operation_log-delete-confirm-modal', 'visible'), + Output('operation_log-delete-ids-store', 'data')], + [Input('operation_log-delete', 'nClicks'), + Input('operation_log-clear', 'nClicks')], + State('operation_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def post_delete_modal(delete_click, clear_click, selected_row_keys): + if delete_click or clear_click: + trigger_id = dash.ctx.triggered_id + if trigger_id == 'operation_log-delete': + oper_ids = ','.join(selected_row_keys) + + return [ + f'是否确认删除日志编号为{oper_ids}的操作日志?', + True, + {'oper_type': 'delete', 'oper_ids': oper_ids} + ] + + elif trigger_id == 'operation_log-clear': + return [ + f'是否确认清除所有的操作日志?', + True, + {'oper_type': 'clear', 'oper_ids': ''} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('operation_log-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('operation_log-delete-confirm-modal', 'okCounts'), + State('operation_log-delete-ids-store', 'data'), + prevent_initial_call=True +) +def operation_log_delete_confirm(delete_confirm, oper_ids_data): + if delete_confirm: + + oper_type = oper_ids_data.get('oper_type') + if oper_type == 'clear': + params = dict(oper_type=oper_ids_data.get('oper_type')) + clear_button_info = clear_operation_log_api(params) + if clear_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除失败', type='error') + ] + else: + params = dict(oper_ids=oper_ids_data.get('oper_ids')) + delete_button_info = delete_operation_log_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 33c21c3..3a6fdee 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -14,6 +14,7 @@ from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_ [Output('post-list-table', 'data', allow_duplicate=True), Output('post-list-table', 'pagination', allow_duplicate=True), Output('post-list-table', 'key'), + Output('post-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('post-search', 'nClicks'), Input('post-list-table', 'pagination'), @@ -72,11 +73,11 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na } if 'system:post:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 69982c6..5c69cf5 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -15,6 +15,7 @@ from api.menu import get_menu_tree_api [Output('role-list-table', 'data', allow_duplicate=True), Output('role-list-table', 'pagination', allow_duplicate=True), Output('role-list-table', 'key'), + Output('role-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('role-search', 'nClicks'), Input('role-list-table', 'pagination'), @@ -83,11 +84,11 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke } if 'system:role:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index 99abb4f..ef43a6e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -37,6 +37,7 @@ def get_search_dept_tree(dept_input): [Output('user-list-table', 'data', allow_duplicate=True), Output('user-list-table', 'pagination', allow_duplicate=True), Output('user-list-table', 'key'), + Output('user-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('dept-tree', 'selectedKeys'), Input('user-search', 'nClicks'), @@ -113,11 +114,11 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio } if 'system:user:resetPwd' in button_perms else None ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 6cbc206..791b673 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -59,5 +59,9 @@ def render_store_container(): dcc.Store(id='post-edit-id-store'), # 岗位管理模块删除操作行key存储容器 dcc.Store(id='post-delete-ids-store'), + # 操作日志管理模块操作类型存储容器 + dcc.Store(id='operation_log-operations-store'), + # 操作日志管理模块删除操作行key存储容器 + dcc.Store(id='operation_log-delete-ids-store'), ] ) diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index b9b2243..382e81c 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -1,8 +1,597 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.monitor_c.operlog_c +from api.log import get_operation_log_list_api + def render(button_perms): - return html.Div('我是操作日志') + operation_log_params = dict(page_num=1, page_size=10) + table_info = get_operation_log_list_api(operation_log_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == 0: + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + if item['business_type'] == 0: + item['business_type'] = dict(tag='其他', color='purple') + elif item['business_type'] == 1: + item['business_type'] = dict(tag='新增', color='green') + elif item['business_type'] == 2: + item['business_type'] = dict(tag='修改', color='orange') + elif item['business_type'] == 3: + item['business_type'] = dict(tag='删除', color='red') + elif item['business_type'] == 4: + item['business_type'] = dict(tag='授权', color='lime') + elif item['business_type'] == 5: + item['business_type'] = dict(tag='导出', color='geekblue') + elif item['business_type'] == 6: + item['business_type'] = dict(tag='导入', color='blue') + elif item['business_type'] == 7: + item['business_type'] = dict(tag='强退', color='magenta') + elif item['business_type'] == 8: + item['business_type'] = dict(tag='生成代码', color='cyan') + elif item['business_type'] == 9: + item['business_type'] = dict(tag='清空数据', color='volcano') + item['key'] = str(item['oper_id']) + item['cost_time'] = f"{item['cost_time']}毫秒" + item['operation'] = [ + { + 'content': '详情', + 'type': 'link', + 'icon': 'antd-eye' + } if 'monitor:operlog:query' in button_perms else {}, + ] + + return [ + dcc.Store(id='operation_log-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='operation_log-title-input', + placeholder='请输入系统模块', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='系统模块', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='operation_log-oper_name-input', + placeholder='请输入操作人员', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='操作人员', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='operation_log-business_type-select', + placeholder='操作类型', + options=[ + { + 'label': '新增', + 'value': 1 + }, + { + 'label': '修改', + 'value': 2 + }, + { + 'label': '删除', + 'value': 3 + }, + { + 'label': '授权', + 'value': 4 + }, + { + 'label': '导出', + 'value': 5 + }, + { + 'label': '导入', + 'value': 6 + }, + { + 'label': '强退', + 'value': 7 + }, + { + 'label': '生成代码', + 'value': 8 + }, + { + 'label': '清空数据', + 'value': 9 + }, + { + 'label': '其他', + 'value': 0 + }, + ], + style={ + 'width': 240 + } + ), + label='类型', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='operation_log-status-select', + placeholder='操作状态', + options=[ + { + 'label': '成功', + 'value': 0 + }, + { + 'label': '失败', + 'value': 1 + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='operation_log-oper_time-range', + style={ + 'width': 240 + } + ), + label='操作时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='operation_log-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='operation_log-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='monitor:operlog:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-delete' + ), + '删除', + ], + id='operation_log-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:operlog:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-clear' + ), + '清空', + ], + id='operation_log-clear', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:operlog:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='operation_log-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='monitor:operlog:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='operation_log-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'oper_id', + 'title': '日志编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'title', + 'title': '系统模块', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'business_type', + 'title': '操作类型', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'oper_name', + 'title': '操作人员', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'oper_ip', + 'title': '操作地址', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'oper_location', + 'title': '操作地点', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '操作状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'oper_time', + 'title': '操作日期', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'cost_time', + 'title': '消耗时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 操作日志明细modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-title-text'), + label='操作模块', + required=True, + id='operation_log-title-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_url-text'), + label='请求地址', + required=True, + id='operation_log-oper_url-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + ], + gutter=5 + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-login_info-text'), + label='登录信息', + required=True, + id='operation_log-login_info-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-request_method-text'), + label='请求方式', + required=True, + id='operation_log-request_method-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + ], + gutter=5 + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-method-text'), + label='操作方法', + required=True, + id='operation_log-method-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_param-text'), + label='请求参数', + required=True, + id='operation_log-oper_param-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-json_result-text'), + label='返回参数', + required=True, + id='operation_log-json_result-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-status-text'), + label='操作状态', + required=True, + id='operation_log-status-form-item', + labelCol={ + 'span': 12 + }, + wrapperCol={ + 'span': 12 + } + ), + span=8 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-cost_time-text'), + label='消耗时间', + required=True, + id='operation_log-cost_time-form-item', + labelCol={ + 'span': 12 + }, + wrapperCol={ + 'span': 12 + } + ), + span=6 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_time-text'), + label='操作时间', + required=True, + id='operation_log-oper_time-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=10 + ), + ], + gutter=5 + ), + ], + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + }, + style={ + 'marginRight': '15px' + } + ) + ], + id='operation_log-modal', + mask=False, + width=850, + renderFooter=False, + ), + + # 删除操作日志二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='operation_log-delete-text'), + id='operation_log-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From bc33bb5316832b54fb542f7e29c0cbd0aa538a3d Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 13 Jul 2023 09:40:15 +0800 Subject: [PATCH 07/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/controller/log_controller.py | 16 + .../module_admin/entity/vo/log_vo.py | 7 + .../module_admin/service/log_service.py | 30 +- dash-fastapi-frontend/api/log.py | 5 + .../callbacks/monitor_c/logininfor_c.py | 187 ++++++++++ .../callbacks/monitor_c/operlog_c.py | 2 +- dash-fastapi-frontend/store/store.py | 4 + .../views/monitor/logininfor/__init__.py | 327 +++++++++++++++++- 8 files changed, 565 insertions(+), 13 deletions(-) create mode 100644 dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index b714327..5d0caba 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -92,3 +92,19 @@ async def delete_system_login_log(request: Request, delete_login_log: DeleteLogi except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@logController.post("/login/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) +@log_decorator(title='操作日志管理', business_type=9) +async def clear_system_login_log(request: Request, clear_login_log: ClearLoginLogModel, query_db: Session = Depends(get_db)): + try: + clear_login_log_result = clear_login_log_services(query_db, clear_login_log) + if clear_login_log_result.is_success: + logger.info(clear_login_log_result.message) + return response_200(data=clear_login_log_result, message=clear_login_log_result.message) + else: + logger.warning(clear_login_log_result.message) + return response_400(data="", message=clear_login_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py index fb778b6..aa38949 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -109,6 +109,13 @@ class DeleteLoginLogModel(BaseModel): info_ids: str +class ClearLoginLogModel(BaseModel): + """ + 清除登录日志模型 + """ + oper_type: str + + class CrudLogResponse(BaseModel): """ 操作各类日志响应模型 diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 06301cc..6aac3d1 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -103,16 +103,28 @@ def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogMod :param page_object: 删除操作日志对象 :return: 删除操作日志校验结果 """ + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') + return CrudLogResponse(**result) + + +def clear_login_log_services(result_db: Session, page_object: ClearLoginLogModel): + """ + 清除操作日志信息service + :param result_db: orm对象 + :param page_object: 清除操作日志对象 + :return: 清除操作日志校验结果 + """ if page_object.oper_type == 'clear': - clear_operation_log_dao(result_db) + clear_login_log_dao(result_db) result = dict(is_success=True, message='清除成功') else: - if page_object.info_ids.split(','): - info_id_list = page_object.info_ids.split(',') - for info_id in info_id_list: - info_id_dict = dict(info_id=info_id) - delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) - result = dict(is_success=True, message='删除成功') - else: - result = dict(is_success=False, message='传入登录日志id为空') + result = dict(is_success=False, message='清除标识不合法') + return CrudLogResponse(**result) diff --git a/dash-fastapi-frontend/api/log.py b/dash-fastapi-frontend/api/log.py index 714af6d..4387c7e 100644 --- a/dash-fastapi-frontend/api/log.py +++ b/dash-fastapi-frontend/api/log.py @@ -29,3 +29,8 @@ def get_login_log_list_api(page_obj: dict): def delete_login_log_api(page_obj: dict): return api_request(method='post', url='/system/log/login/delete', is_headers=True, json=page_obj) + + +def clear_login_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/clear', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py new file mode 100644 index 0000000..143eb2c --- /dev/null +++ b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py @@ -0,0 +1,187 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.log import get_login_log_list_api, delete_login_log_api, clear_login_log_api + + +@app.callback( + [Output('login_log-list-table', 'data', allow_duplicate=True), + Output('login_log-list-table', 'pagination', allow_duplicate=True), + Output('login_log-list-table', 'key'), + Output('login_log-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('login_log-search', 'nClicks'), + Input('login_log-list-table', 'pagination'), + Input('login_log-operations-store', 'data')], + [State('login_log-ipaddr-input', 'value'), + State('login_log-user_name-input', 'value'), + State('login_log-status-select', 'value'), + State('login_log-login_time-range', 'value'), + State('login_log-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_login_log_table_data(search_click, pagination, operations, ipaddr, user_name, status_select, login_time_range, button_perms): + + login_time_start = None + login_time_end = None + if login_time_range: + login_time_start = login_time_range[0] + login_time_end = login_time_range[1] + query_params = dict( + ipaddr=ipaddr, + user_name=user_name, + status=status_select, + login_time_start=login_time_start, + login_time_end=login_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + ipaddr=ipaddr, + user_name=user_name, + status=status_select, + login_time_start=login_time_start, + login_time_end=login_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_login_log_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + item['key'] = str(item['info_id']) + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('login_log-ipaddr-input', 'value'), + Output('login_log-user_name-input', 'value'), + Output('login_log-status-select', 'value'), + Output('login_log-login_time-range', 'value'), + Output('login_log-operations-store', 'data')], + Input('login_log-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_login_log_query_params(reset_click): + if reset_click: + return [None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('login_log-delete', 'disabled'), + Output('login_log-unlock', 'disabled')], + Input('login_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_login_log_delete_unlock_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [False, True] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('login_log-delete-text', 'children'), + Output('login_log-delete-confirm-modal', 'visible'), + Output('login_log-delete-ids-store', 'data')], + [Input('login_log-delete', 'nClicks'), + Input('login_log-clear', 'nClicks')], + State('login_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def login_log_delete_modal(delete_click, clear_click, selected_row_keys): + if delete_click or clear_click: + trigger_id = dash.ctx.triggered_id + if trigger_id == 'login_log-delete': + info_ids = ','.join(selected_row_keys) + + return [ + f'是否确认删除访问编号为{info_ids}的操作日志?', + True, + {'oper_type': 'delete', 'info_ids': info_ids} + ] + + elif trigger_id == 'login_log-clear': + return [ + f'是否确认清除所有的登录日志?', + True, + {'oper_type': 'clear', 'info_ids': ''} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('login_log-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('login_log-delete-confirm-modal', 'okCounts'), + State('login_log-delete-ids-store', 'data'), + prevent_initial_call=True +) +def login_log_delete_confirm(delete_confirm, info_ids_data): + if delete_confirm: + + oper_type = info_ids_data.get('oper_type') + if oper_type == 'clear': + params = dict(oper_type=info_ids_data.get('oper_type')) + clear_button_info = clear_login_log_api(params) + if clear_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除失败', type='error') + ] + else: + params = dict(info_ids=info_ids_data.get('info_ids')) + delete_button_info = delete_login_log_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py index 09dfc19..eb26438 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -200,7 +200,7 @@ def change_operation_log_delete_button_status(table_rows_selected): State('operation_log-list-table', 'selectedRowKeys'), prevent_initial_call=True ) -def post_delete_modal(delete_click, clear_click, selected_row_keys): +def operation_log_delete_modal(delete_click, clear_click, selected_row_keys): if delete_click or clear_click: trigger_id = dash.ctx.triggered_id if trigger_id == 'operation_log-delete': diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 791b673..21f88d5 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -63,5 +63,9 @@ def render_store_container(): dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 dcc.Store(id='operation_log-delete-ids-store'), + # 登录日志管理模块操作类型存储容器 + dcc.Store(id='login_log-operations-store'), + # 操作日志管理模块删除操作行key存储容器 + dcc.Store(id='login_log-delete-ids-store'), ] ) diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py index c3e2dbb..3ba6c94 100644 --- a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -1,8 +1,329 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.monitor_c.logininfor_c +from api.log import get_login_log_list_api + def render(button_perms): - return html.Div('我是登录日志') + login_log_params = dict(page_num=1, page_size=10) + table_info = get_login_log_list_api(login_log_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + item['key'] = str(item['info_id']) + + return [ + dcc.Store(id='login_log-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='login_log-ipaddr-input', + placeholder='请输入登录地址', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='登录地址', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='login_log-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='用户名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='login_log-status-select', + placeholder='登录状态', + options=[ + { + 'label': '成功', + 'value': 0 + }, + { + 'label': '失败', + 'value': 1 + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='login_log-login_time-range', + style={ + 'width': 240 + } + ), + label='登录时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='login_log-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='login_log-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='monitor:logininfor:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-delete' + ), + '删除', + ], + id='login_log-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:logininfor:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-clear' + ), + '清空', + ], + id='login_log-clear', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:logininfor:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-unlock' + ), + '解锁', + ], + id='login_log-unlock', + disabled=True, + style={ + 'color': '#74bcff', + 'background': '#e8f4ff', + 'border-color': '#d1e9ff' + } + ), + ], + hidden='monitor:logininfor:unlock' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='login_log-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='monitor:logininfor:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='login_log-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'info_id', + 'title': '访问编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'user_name', + 'title': '用户名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'ipaddr', + 'title': '登录地址', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'login_location', + 'title': '登录地点', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'browser', + 'title': '浏览器', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'os', + 'title': '操作系统', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '登录状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'msg', + 'title': '操作信息', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'login_time', + 'title': '登录日期', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 删除操作日志二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='login_log-delete-text'), + id='login_log-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From e1dbd6a97dae42b97095d2cb7d890ad03e530d93 Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 13 Jul 2023 17:39:14 +0800 Subject: [PATCH 08/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E7=AE=A1=E7=90=86=EF=BC=9B=20fix:=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../controller/dict_controller.py | 163 +++++++ .../module_admin/controller/log_controller.py | 2 +- .../module_admin/dao/dict_dao.py | 201 ++++++++ .../module_admin/entity/vo/dict_vo.py | 105 ++++ .../module_admin/service/dept_service.py | 2 +- .../module_admin/service/dict_service.py | 136 ++++++ .../module_admin/service/menu_service.py | 2 +- .../module_admin/service/post_service.py | 2 +- dash-fastapi-frontend/api/dict.py | 51 ++ .../callbacks/system_c/dict_c.py | 333 +++++++++++++ .../callbacks/system_c/role_c.py | 34 +- dash-fastapi-frontend/store/store.py | 7 + .../views/system/dict/__init__.py | 458 +++++++++++++++++- 14 files changed, 1475 insertions(+), 23 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/controller/dict_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/dict_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/dict_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/dict_service.py create mode 100644 dash-fastapi-frontend/api/dict.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/dict_c.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 613d269..246d710 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -11,6 +11,7 @@ from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController +from module_admin.controller.dict_controller import dictController from module_admin.controller.log_controller import logController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -76,6 +77,7 @@ app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) +app.include_router(dictController, prefix="/system", tags=['system/dict']) app.include_router(logController, prefix="/system", tags=['system/log']) diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py new file mode 100644 index 0000000..28ab00e --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -0,0 +1,163 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.dict_service import * +from module_admin.entity.vo.dict_vo import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +dictController = APIRouter(dependencies=[Depends(get_current_user)]) + + +@dictController.post("/dictType/get", response_model=DictTypePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_dict_type_list(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): + try: + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + logger.info('获取成功') + return response_200(data=dict_type_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictType/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@log_decorator(title='字典管理', business_type=1) +async def add_system_dict_type(request: Request, add_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_dict_type.create_by = current_user.user.user_name + add_dict_type.update_by = current_user.user.user_name + add_dict_type_result = add_dict_type_services(query_db, add_dict_type) + logger.info(add_dict_type_result.message) + if add_dict_type_result.is_success: + return response_200(data=add_dict_type_result, message=add_dict_type_result.message) + else: + return response_400(data="", message=add_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.patch("/dictType/edit", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@log_decorator(title='字典管理', business_type=2) +async def edit_system_dict_type(request: Request, edit_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_dict_type.update_by = current_user.user.user_name + edit_dict_type.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dict_type_result = edit_dict_type_services(query_db, edit_dict_type) + if edit_dict_type_result.is_success: + logger.info(edit_dict_type_result.message) + return response_200(data=edit_dict_type_result, message=edit_dict_type_result.message) + else: + logger.warning(edit_dict_type_result.message) + return response_400(data="", message=edit_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictType/delete", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@log_decorator(title='字典管理', business_type=3) +async def delete_system_dict_type(request: Request, delete_dict_type: DeleteDictTypeModel, query_db: Session = Depends(get_db)): + try: + delete_dict_type_result = delete_dict_type_services(query_db, delete_dict_type) + if delete_dict_type_result.is_success: + logger.info(delete_dict_type_result.message) + return response_200(data=delete_dict_type_result, message=delete_dict_type_result.message) + else: + logger.warning(delete_dict_type_result.message) + return response_400(data="", message=delete_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.get("/dictType/{dict_id}", response_model=DictTypeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +async def query_detail_system_dict_type(request: Request, dict_id: int, query_db: Session = Depends(get_db)): + try: + detail_dict_type_result = detail_dict_type_services(query_db, dict_id) + logger.info(f'获取dict_id为{dict_id}的信息成功') + return response_200(data=detail_dict_type_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/get", response_model=DictDataPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_dict_data_list(request: Request, dict_data_query: DictDataPageObject, query_db: Session = Depends(get_db)): + try: + dict_data_query_result = get_dict_data_list(query_db, dict_data_query) + logger.info('获取成功') + return response_200(data=dict_data_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@log_decorator(title='字典管理', business_type=1) +async def add_system_dict_data(request: Request, add_dict_data: DictDataModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_dict_data.create_by = current_user.user.user_name + add_dict_data.update_by = current_user.user.user_name + add_dict_data_result = add_dict_data_services(query_db, add_dict_data) + logger.info(add_dict_data_result.message) + if add_dict_data_result.is_success: + return response_200(data=add_dict_data_result, message=add_dict_data_result.message) + else: + return response_400(data="", message=add_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.patch("/dictData/edit", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@log_decorator(title='字典管理', business_type=2) +async def edit_system_dict_data(request: Request, edit_dict_data: DictDataModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_dict_data.update_by = current_user.user.user_name + edit_dict_data.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dict_data_result = edit_dict_data_services(query_db, edit_dict_data) + if edit_dict_data_result.is_success: + logger.info(edit_dict_data_result.message) + return response_200(data=edit_dict_data_result, message=edit_dict_data_result.message) + else: + logger.warning(edit_dict_data_result.message) + return response_400(data="", message=edit_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/delete", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@log_decorator(title='字典管理', business_type=3) +async def delete_system_dict_data(request: Request, delete_dict_data: DeleteDictDataModel, query_db: Session = Depends(get_db)): + try: + delete_dict_data_result = delete_dict_data_services(query_db, delete_dict_data) + if delete_dict_data_result.is_success: + logger.info(delete_dict_data_result.message) + return response_200(data=delete_dict_data_result, message=delete_dict_data_result.message) + else: + logger.warning(delete_dict_data_result.message) + return response_400(data="", message=delete_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.get("/dictData/{dict_code}", response_model=DictDataModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +async def query_detail_system_dict_data(request: Request, dict_code: int, query_db: Session = Depends(get_db)): + try: + detail_dict_data_result = detail_dict_data_services(query_db, dict_code) + logger.info(f'获取dict_code为{dict_code}的信息成功') + return response_200(data=detail_dict_data_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index 5d0caba..c5ef771 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -95,7 +95,7 @@ async def delete_system_login_log(request: Request, delete_login_log: DeleteLogi @logController.post("/login/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) -@log_decorator(title='操作日志管理', business_type=9) +@log_decorator(title='登录日志管理', business_type=9) async def clear_system_login_log(request: Request, clear_login_log: ClearLoginLogModel, query_db: Session = Depends(get_db)): try: clear_login_log_result = clear_login_log_services(query_db, clear_login_log) diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py new file mode 100644 index 0000000..6146099 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -0,0 +1,201 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.dict_do import SysDictType, SysDictData +from module_admin.entity.vo.dict_vo import DictTypeModel, DictTypePageObject, DictTypePageObjectResponse, \ + DictDataModel, DictDataPageObject, DictDataPageObjectResponse, CrudDictResponse +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info +from datetime import datetime, time + + +def get_dict_type_detail_by_id(db: Session, dict_id: int): + dict_type_info = db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_id) \ + .first() + + return dict_type_info + + +def get_dict_type_list(db: Session, page_object: DictTypePageObject): + """ + 根据查询参数获取字典类型列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典类型列表信息对象 + """ + count = db.query(SysDictType) \ + .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, + SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, + SysDictType.status == page_object.status if page_object.status else True, + SysDictType.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + dict_type_list = db.query(SysDictType) \ + .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, + SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, + SysDictType.status == page_object.status if page_object.status else True, + SysDictType.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(dict_type_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return DictTypePageObjectResponse(**result) + + +def add_dict_type_dao(db: Session, dict_type: DictTypeModel): + """ + 新增字典类型数据库操作 + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: 新增校验结果 + """ + db_dict_type = SysDictType(**dict_type.dict()) + db.add(db_dict_type) + db.commit() # 提交保存到数据库中 + db.refresh(db_dict_type) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudDictResponse(**result) + + +def edit_dict_type_dao(db: Session, dict_type: dict): + """ + 编辑字典类型数据库操作 + :param db: orm对象 + :param dict_type: 需要更新的字典类型字典 + :return: 编辑校验结果 + """ + is_dict_type_id = db.query(SysDictType).filter(SysDictType.dict_id == dict_type.get('dict_id')).all() + if not is_dict_type_id: + result = dict(is_success=False, message='字典类型不存在') + else: + db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_type.get('dict_id')) \ + .update(dict_type) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudDictResponse(**result) + + +def delete_dict_type_dao(db: Session, dict_type: DictTypeModel): + """ + 删除字典类型数据库操作 + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: + """ + db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_type.dict_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def get_dict_data_detail_by_id(db: Session, dict_code: int): + dict_data_info = db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_code) \ + .first() + + return dict_data_info + + +def get_dict_data_list(db: Session, page_object: DictDataPageObject): + """ + 根据查询参数获取字典数据列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典数据列表信息对象 + """ + count = db.query(SysDictData) \ + .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, + SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, + SysDictData.status == page_object.status if page_object.status else True + )\ + .order_by(SysDictData.dict_sort)\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + dict_data_list = db.query(SysDictData) \ + .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, + SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, + SysDictData.status == page_object.status if page_object.status else True + )\ + .order_by(SysDictData.dict_sort)\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(dict_data_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return DictDataPageObjectResponse(**result) + + +def add_dict_data_dao(db: Session, dict_data: DictDataModel): + """ + 新增字典数据数据库操作 + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: 新增校验结果 + """ + db_data_type = SysDictData(**dict_data.dict()) + db.add(db_data_type) + db.commit() # 提交保存到数据库中 + db.refresh(db_data_type) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudDictResponse(**result) + + +def edit_dict_data_dao(db: Session, dict_data: dict): + """ + 编辑字典数据数据库操作 + :param db: orm对象 + :param dict_data: 需要更新的字典数据字典 + :return: 编辑校验结果 + """ + is_dict_data_id = db.query(SysDictData).filter(SysDictData.dict_code == dict_data.get('dict_code')).all() + if not is_dict_data_id: + result = dict(is_success=False, message='字典数据不存在') + else: + db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_data.get('dict_code')) \ + .update(dict_data) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudDictResponse(**result) + + +def delete_dict_data_dao(db: Session, dict_data: DictDataModel): + """ + 删除字典数据数据库操作 + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: + """ + db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_data.dict_code) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py b/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py new file mode 100644 index 0000000..e7b9fe6 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py @@ -0,0 +1,105 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class DictTypeModel(BaseModel): + """ + 字典类型表对应pydantic模型 + """ + dict_id: Optional[int] + dict_name: Optional[str] + dict_type: Optional[str] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class DictDataModel(BaseModel): + """ + 字典数据表对应pydantic模型 + """ + dict_code: Optional[int] + dict_sort: Optional[int] + dict_label: Optional[str] + dict_value: Optional[str] + dict_type: Optional[str] + css_class: Optional[str] + list_class: Optional[str] + is_default: Optional[str] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class DictTypePageObject(DictTypeModel): + """ + 字典类型管理分页查询模型 + """ + create_time_start: Optional[str] + create_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class DictTypePageObjectResponse(BaseModel): + """ + 字典类型管理列表分页查询返回模型 + """ + rows: List[Union[DictTypeModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteDictTypeModel(BaseModel): + """ + 删除字典类型模型 + """ + dict_ids: str + + +class DictDataPageObject(DictDataModel): + """ + 字典数据管理分页查询模型 + """ + page_num: Optional[int] + page_size: Optional[int] + + +class DictDataPageObjectResponse(BaseModel): + """ + 字典数据管理列表分页查询返回模型 + """ + rows: List[Union[DictDataModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteDictDataModel(BaseModel): + """ + 删除字典数据模型 + """ + dict_codes: str + + +class CrudDictResponse(BaseModel): + """ + 操作字典响应模型 + """ + is_success: bool + message: str diff --git a/dash-fastapi-backend/module_admin/service/dept_service.py b/dash-fastapi-backend/module_admin/service/dept_service.py index 682706e..383bfb3 100644 --- a/dash-fastapi-backend/module_admin/service/dept_service.py +++ b/dash-fastapi-backend/module_admin/service/dept_service.py @@ -102,7 +102,7 @@ def delete_dept_services(result_db: Session, page_object: DeleteDeptModel): delete_dept_dao(result_db, DeptModel(**dept_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入部门id为空') return CrudDeptResponse(**result) diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py new file mode 100644 index 0000000..70a16c1 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -0,0 +1,136 @@ +from module_admin.entity.vo.dict_vo import * +from module_admin.dao.dict_dao import * + + +def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObject): + """ + 获取字典类型列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典类型列表信息对象 + """ + dict_type_list_result = get_dict_type_list(result_db, page_object) + + return dict_type_list_result + + +def add_dict_type_services(result_db: Session, page_object: DictTypeModel): + """ + 新增字典类型信息service + :param result_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典类型校验结果 + """ + add_dict_type_result = add_dict_type_dao(result_db, page_object) + + return add_dict_type_result + + +def edit_dict_type_services(result_db: Session, page_object: DictTypeModel): + """ + 编辑字典类型信息service + :param result_db: orm对象 + :param page_object: 编辑字典类型对象 + :return: 编辑字典类型校验结果 + """ + edit_dict_type = page_object.dict(exclude_unset=True) + edit_dict_type_result = edit_dict_type_dao(result_db, edit_dict_type) + + return edit_dict_type_result + + +def delete_dict_type_services(result_db: Session, page_object: DeleteDictTypeModel): + """ + 删除字典类型信息service + :param result_db: orm对象 + :param page_object: 删除字典类型对象 + :return: 删除字典类型校验结果 + """ + if page_object.dict_ids.split(','): + dict_id_list = page_object.dict_ids.split(',') + for dict_id in dict_id_list: + dict_id_dict = dict(dict_id=dict_id) + delete_dict_type_dao(result_db, DictTypeModel(**dict_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入字典类型id为空') + return CrudDictResponse(**result) + + +def detail_dict_type_services(result_db: Session, dict_id: int): + """ + 获取字典类型详细信息service + :param result_db: orm对象 + :param dict_id: 字典类型id + :return: 字典类型id对应的信息 + """ + dict_type = get_dict_type_detail_by_id(result_db, dict_id=dict_id) + + return dict_type + + +def get_dict_data_list_services(result_db: Session, page_object: DictDataPageObject): + """ + 获取字典数据列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典数据列表信息对象 + """ + dict_data_list_result = get_dict_data_list(result_db, page_object) + + return dict_data_list_result + + +def add_dict_data_services(result_db: Session, page_object: DictDataModel): + """ + 新增字典数据信息service + :param result_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典数据校验结果 + """ + add_dict_data_result = add_dict_data_dao(result_db, page_object) + + return add_dict_data_result + + +def edit_dict_data_services(result_db: Session, page_object: DictDataModel): + """ + 编辑字典数据信息service + :param result_db: orm对象 + :param page_object: 编辑字典数据对象 + :return: 编辑字典数据校验结果 + """ + edit_data_type = page_object.dict(exclude_unset=True) + edit_dict_data_result = edit_dict_data_dao(result_db, edit_data_type) + + return edit_dict_data_result + + +def delete_dict_data_services(result_db: Session, page_object: DeleteDictDataModel): + """ + 删除字典数据信息service + :param result_db: orm对象 + :param page_object: 删除字典数据对象 + :return: 删除字典数据校验结果 + """ + if page_object.dict_codes.split(','): + dict_code_list = page_object.dict_codes.split(',') + for dict_code in dict_code_list: + dict_code_dict = dict(dict_code=dict_code) + delete_dict_data_dao(result_db, DictDataModel(**dict_code_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入字典数据id为空') + return CrudDictResponse(**result) + + +def detail_dict_data_services(result_db: Session, dict_code: int): + """ + 获取字典数据详细信息service + :param result_db: orm对象 + :param dict_code: 字典数据id + :return: 字典数据id对应的信息 + """ + dict_data = get_dict_data_detail_by_id(result_db, dict_code=dict_code) + + return dict_data diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index 2c102b5..adebaa8 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -86,7 +86,7 @@ def delete_menu_services(result_db: Session, page_object: DeleteMenuModel): delete_menu_dao(result_db, MenuModel(**menu_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入菜单id为空') return CrudMenuResponse(**result) diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index f048a05..507f477 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -64,7 +64,7 @@ def delete_post_services(result_db: Session, page_object: DeletePostModel): delete_post_dao(result_db, PostModel(**post_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入岗位id为空') return CrudPostResponse(**result) diff --git a/dash-fastapi-frontend/api/dict.py b/dash-fastapi-frontend/api/dict.py new file mode 100644 index 0000000..c412531 --- /dev/null +++ b/dash-fastapi-frontend/api/dict.py @@ -0,0 +1,51 @@ +from utils.request import api_request + + +def get_dict_type_list_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/get', is_headers=True, json=page_obj) + + +def add_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/add', is_headers=True, json=page_obj) + + +def edit_dict_type_api(page_obj: dict): + + return api_request(method='patch', url='/system/dictType/edit', is_headers=True, json=page_obj) + + +def delete_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/delete', is_headers=True, json=page_obj) + + +def get_dict_type_detail_api(dict_id: int): + + return api_request(method='get', url=f'/system/dictType/{dict_id}', is_headers=True) + + +def get_dict_data_list_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/get', is_headers=True, json=page_obj) + + +def add_dict_data_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/add', is_headers=True, json=page_obj) + + +def edit_dict_data_api(page_obj: dict): + + return api_request(method='patch', url='/system/dictData/edit', is_headers=True, json=page_obj) + + +def delete_dict_data_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/delete', is_headers=True, json=page_obj) + + +def get_dict_data_detail_api(dict_id: int): + + return api_request(method='get', url=f'/system/dictData/{dict_id}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c.py new file mode 100644 index 0000000..c7c154d --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c.py @@ -0,0 +1,333 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.dict import get_dict_type_list_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api + + +@app.callback( + [Output('dict_type-list-table', 'data', allow_duplicate=True), + Output('dict_type-list-table', 'pagination', allow_duplicate=True), + Output('dict_type-list-table', 'key'), + Output('dict_type-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dict_type-search', 'nClicks'), + Input('dict_type-list-table', 'pagination'), + Input('dict_type-operations-store', 'data')], + [State('dict_type-dict_name-input', 'value'), + State('dict_type-dict_type-input', 'value'), + State('dict_type-status-select', 'value'), + State('dict_type-create_time-range', 'value'), + State('dict_type-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_dict_type_table_data(search_click, pagination, operations, dict_name, dict_type, status_select, create_time_range, button_perms): + create_time_start = None + create_time_end = None + if create_time_range: + create_time_start = create_time_range[0] + create_time_end = create_time_range[1] + + query_params = dict( + dict_name=dict_name, + dict_type=dict_type, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + dict_name=dict_name, + dict_type=dict_type, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_dict_type_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_id']) + item['dict_type'] = [ + { + 'content': item['dict_type'], + 'type': 'link', + } + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_type-dict_name-input', 'value'), + Output('dict_type-dict_type-input', 'value'), + Output('dict_type-status-select', 'value'), + Output('dict_type-create_time-range', 'value'), + Output('dict_type-operations-store', 'data')], + Input('dict_type-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_dict_type_query_params(reset_click): + if reset_click: + return [None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_type-edit', 'disabled'), + Output('dict_type-delete', 'disabled')], + Input('dict_type-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_dict_type_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('dict_type-modal', 'visible', allow_duplicate=True), + Output('dict_type-modal', 'title'), + Output('dict_type-dict_name', 'value'), + Output('dict_type-dict_type', 'value'), + Output('dict_type-status', 'value'), + Output('dict_type-remark', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('dict_type-add', 'nClicks'), + Output('dict_type-edit', 'nClicks'), + Output('dict_type-edit-id-store', 'data'), + Output('dict_type-operations-store-bk', 'data')], + [Input('dict_type-add', 'nClicks'), + Input('dict_type-edit', 'nClicks'), + Input('dict_type-list-table', 'nClicksButton')], + [State('dict_type-list-table', 'selectedRowKeys'), + State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_edit_dict_type_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, + recently_button_clicked_row): + if add_click or edit_click or button_click: + if add_click: + return [ + True, + '新增字典类型', + None, + None, + '0', + None, + {'timestamp': time.time()}, + None, + None, + None, + {'type': 'add'} + ] + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + dict_id = int(','.join(selected_row_keys)) + else: + dict_id = int(recently_button_clicked_row['key']) + dict_type_info_res = get_dict_type_detail_api(dict_id=dict_id) + if dict_type_info_res['code'] == 200: + dict_type_info = dict_type_info_res['data'] + return [ + True, + '编辑字典类型', + dict_type_info.get('dict_name'), + dict_type_info.get('dict_type'), + dict_type_info.get('status'), + dict_type_info.get('remark'), + {'timestamp': time.time()}, + None, + None, + dict_type_info if dict_type_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 6 + [{'timestamp': time.time()}, None, None, None, None] + + return [dash.no_update] * 7 + [None, None, None, None] + + +@app.callback( + [Output('dict_type-dict_name-form-item', 'validateStatus'), + Output('dict_type-dict_type-form-item', 'validateStatus'), + Output('dict_type-dict_name-form-item', 'help'), + Output('dict_type-dict_type-form-item', 'help'), + Output('dict_type-modal', 'visible'), + Output('dict_type-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_type-modal', 'okCounts'), + [State('dict_type-operations-store-bk', 'data'), + State('dict_type-edit-id-store', 'data'), + State('dict_type-dict_name', 'value'), + State('dict_type-dict_type', 'value'), + State('dict_type-status', 'value'), + State('dict_type-remark', 'value')], + prevent_initial_call=True +) +def dict_type_confirm(confirm_trigger, operation_type, cur_post_info, dict_name, dict_type, status, remark): + if confirm_trigger: + if all([dict_name, dict_type]): + params_add = dict(dict_name=dict_name, dict_type=dict_type, status=status, remark=remark) + params_edit = dict(dict_id=cur_post_info.get('dict_id') if cur_post_info else None, dict_name=dict_name, + dict_type=dict_type, status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_dict_type_api(params_add) + if operation_type == 'edit': + api_res = edit_dict_type_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if dict_name else 'error', + None if dict_type else 'error', + None if dict_name else '请输入字典名称!', + None if dict_type else '请输入字典类型!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 8 + + +@app.callback( + [Output('dict_type-delete-text', 'children'), + Output('dict_type-delete-confirm-modal', 'visible'), + Output('dict_type-delete-ids-store', 'data')], + [Input('dict_type-delete', 'nClicks'), + Input('dict_type-list-table', 'nClicksButton')], + [State('dict_type-list-table', 'selectedRowKeys'), + State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dict_type_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'dict_type-delete': + dict_ids = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + dict_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除字典编号为{dict_ids}的岗位?', + True, + {'dict_ids': dict_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_type-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_type-delete-confirm-modal', 'okCounts'), + State('dict_type-delete-ids-store', 'data'), + prevent_initial_call=True +) +def dict_type_delete_confirm(delete_confirm, dict_ids_data): + if delete_confirm: + + params = dict_ids_data + delete_button_info = delete_dict_type_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 5c69cf5..87ad59e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -175,14 +175,15 @@ def all_none_role_menu_mode(all_none, menu_info): def change_role_menu_mode(parent_children, current_role_menu): if parent_children: checked_menu = [] - for item in current_role_menu: - has_children = False - for other_item in current_role_menu: - if other_item['parent_id'] == item['menu_id']: - has_children = True - break - if not has_children: - checked_menu.append(str(item.get('menu_id'))) + if current_role_menu[0]: + for item in current_role_menu: + has_children = False + for other_item in current_role_menu: + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [False, checked_menu] else: checked_menu = [str(item.get('menu_id')) for item in current_role_menu if item] or [] @@ -250,14 +251,15 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, if role_info_res['code'] == 200: role_info = role_info_res['data'] checked_menu = [] - for item in role_info.get('menu'): - has_children = False - for other_item in role_info.get('menu'): - if other_item['parent_id'] == item['menu_id']: - has_children = True - break - if not has_children: - checked_menu.append(str(item.get('menu_id'))) + if role_info.get('menu')[0]: + for item in role_info.get('menu'): + has_children = False + for other_item in role_info.get('menu'): + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [ True, '编辑角色', diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 21f88d5..f9ad59f 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -59,6 +59,13 @@ def render_store_container(): dcc.Store(id='post-edit-id-store'), # 岗位管理模块删除操作行key存储容器 dcc.Store(id='post-delete-ids-store'), + # 字典管理模块操作类型存储容器 + dcc.Store(id='dict_type-operations-store'), + dcc.Store(id='dict_type-operations-store-bk'), + # 字典管理模块修改操作行key存储容器 + dcc.Store(id='dict_type-edit-id-store'), + # 字典管理模块删除操作行key存储容器 + dcc.Store(id='dict_type-delete-ids-store'), # 操作日志管理模块操作类型存储容器 dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index e0aa134..fd264a0 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -1,8 +1,460 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.system_c.dict_c +from api.dict import get_dict_type_list_api + def render(button_perms): - return html.Div('我是字典管理') + dict_type_params = dict(page_num=1, page_size=10) + table_info = get_dict_type_list_api(dict_type_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_id']) + item['dict_type'] = [ + { + 'content': item['dict_type'], + 'type': 'link', + } + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [ + dcc.Store(id='dict_type-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_name-input', + placeholder='请输入字典名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_type-input', + placeholder='请输入字典类型', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典类型', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dict_type-status-select', + placeholder='字典状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='dict_type-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dict_type-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dict_type-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='system:dict:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dict_type-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + ], + hidden='system:dict:add' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='dict_type-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + ], + hidden='system:dict:edit' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='dict_type-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='system:dict:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='dict_type-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='system:dict:export' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-sync' + ), + '刷新缓存', + ], + id='dict_type-refresh', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dict_type-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'dict_id', + 'title': '字典编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_name', + 'title': '字典名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_type', + 'title': '字典类型', + 'renderOptions': { + 'renderType': 'button' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'remark', + 'title': '备注', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑字典类型表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_name', + placeholder='请输入字典名称', + allowClear=True, + style={ + 'width': 350 + } + ), + label='字典名称', + required=True, + id='dict_type-dict_name-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_type', + placeholder='请输入字典类型', + allowClear=True, + style={ + 'width': 350 + } + ), + label='字典类型', + required=True, + id='dict_type-dict_type-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dict_type-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='状态', + id='dict_type-status-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='dict_type-remark-form-item' + ), + span=24 + ), + ] + ), + ], + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ) + ], + id='dict_type-modal', + mask=False, + width=580, + renderFooter=True, + okClickClose=False + ), + + # 删除字典类型二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='dict_type-delete-text'), + id='dict_type-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From cc86bcff897e383ffa941a537bccb52920008ef5 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 15 Jul 2023 00:08:18 +0800 Subject: [PATCH 09/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dict_controller.py | 11 + .../module_admin/dao/dict_dao.py | 6 + .../module_admin/service/dict_service.py | 5 +- dash-fastapi-frontend/api/dict.py | 9 +- .../callbacks/system_c/{ => dict_c}/dict_c.py | 55 +- .../callbacks/system_c/dict_c/dict_data_c.py | 342 ++++++++++++ dash-fastapi-frontend/store/store.py | 4 + .../views/system/dict/__init__.py | 24 +- .../views/system/dict/dict_data.py | 504 ++++++++++++++++++ 9 files changed, 943 insertions(+), 17 deletions(-) rename dash-fastapi-frontend/callbacks/system_c/{ => dict_c}/dict_c.py (85%) create mode 100644 dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py create mode 100644 dash-fastapi-frontend/views/system/dict/dict_data.py diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py index 28ab00e..4634e06 100644 --- a/dash-fastapi-backend/module_admin/controller/dict_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -24,6 +24,17 @@ async def get_system_dict_type_list(request: Request, dict_type_query: DictTypeP return response_500(data="", message="接口异常") +@dictController.post("/dictType/all", dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_all_dict_type(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): + try: + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + logger.info('获取成功') + return response_200(data=dict_type_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + @dictController.post("/dictType/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) @log_decorator(title='字典管理', business_type=1) async def add_system_dict_type(request: Request, add_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py index 6146099..c15f36d 100644 --- a/dash-fastapi-backend/module_admin/dao/dict_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -15,6 +15,12 @@ def get_dict_type_detail_by_id(db: Session, dict_id: int): return dict_type_info +def get_all_dict_type(db: Session): + dict_type_info = db.query(SysDictType).all() + + return list_format_datetime(dict_type_info) + + def get_dict_type_list(db: Session, page_object: DictTypePageObject): """ 根据查询参数获取字典类型列表信息 diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py index 70a16c1..9069456 100644 --- a/dash-fastapi-backend/module_admin/service/dict_service.py +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -9,7 +9,10 @@ def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObj :param page_object: 分页查询参数对象 :return: 字典类型列表信息对象 """ - dict_type_list_result = get_dict_type_list(result_db, page_object) + if page_object.page_num and page_object.page_size: + dict_type_list_result = get_dict_type_list(result_db, page_object) + else: + dict_type_list_result = get_all_dict_type(result_db) return dict_type_list_result diff --git a/dash-fastapi-frontend/api/dict.py b/dash-fastapi-frontend/api/dict.py index c412531..96d440c 100644 --- a/dash-fastapi-frontend/api/dict.py +++ b/dash-fastapi-frontend/api/dict.py @@ -6,6 +6,11 @@ def get_dict_type_list_api(page_obj: dict): return api_request(method='post', url='/system/dictType/get', is_headers=True, json=page_obj) +def get_all_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/all', is_headers=True, json=page_obj) + + def add_dict_type_api(page_obj: dict): return api_request(method='post', url='/system/dictType/add', is_headers=True, json=page_obj) @@ -46,6 +51,6 @@ def delete_dict_data_api(page_obj: dict): return api_request(method='post', url='/system/dictData/delete', is_headers=True, json=page_obj) -def get_dict_data_detail_api(dict_id: int): +def get_dict_data_detail_api(dict_code: int): - return api_request(method='get', url=f'/system/dictData/{dict_id}', is_headers=True) + return api_request(method='get', url=f'/system/dictData/{dict_code}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py similarity index 85% rename from dash-fastapi-frontend/callbacks/system_c/dict_c.py rename to dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py index c7c154d..4ab9899 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py @@ -7,7 +7,7 @@ import feffery_antd_components as fac import feffery_utils_components as fuc from server import app -from api.dict import get_dict_type_list_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api +from api.dict import get_dict_type_list_api, get_all_dict_type_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api @app.callback( @@ -70,12 +70,10 @@ def get_dict_type_table_data(search_click, pagination, operations, dict_name, di else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dict_id']) - item['dict_type'] = [ - { - 'content': item['dict_type'], - 'type': 'link', - } - ] + item['dict_type'] = { + 'content': item['dict_type'], + 'type': 'link', + } item['operation'] = [ { 'content': '修改', @@ -331,3 +329,46 @@ def dict_type_delete_confirm(delete_confirm, dict_ids_data): ] return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_type_to_dict_data-modal', 'visible'), + Output('dict_type_to_dict_data-modal', 'title'), + Output('dict_data-dict_type-select', 'options'), + Output('dict_data-dict_type-select', 'value', allow_duplicate=True), + Output('dict_data-search', 'nClicks'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('dict_type-list-table', 'nClicksButton'), + [State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow'), + State('dict_data-search', 'nClicks')], + prevent_initial_call=True +) +def dict_type_to_dict_data_modal(button_click, clicked_content, recently_button_clicked_row, dict_data_search_nclick): + + if button_click and clicked_content == recently_button_clicked_row.get('dict_type').get('content'): + all_dict_type_info = get_all_dict_type_api({}) + if all_dict_type_info.get('code') == 200: + all_dict_type = all_dict_type_info.get('data') + dict_data_options = [dict(label=item.get('dict_name'), value=item.get('dict_type')) for item in all_dict_type] + + return [ + True, + '字典数据', + dict_data_options, + recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + {'timestamp': time.time()}, + ] + + return [ + True, + '字典数据', + [], + recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + {'timestamp': time.time()}, + ] + + return [dash.no_update] * 6 + diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py new file mode 100644 index 0000000..6ea7dd8 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py @@ -0,0 +1,342 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.dict import get_dict_data_list_api, get_dict_data_detail_api, add_dict_data_api, edit_dict_data_api, delete_dict_data_api + + +@app.callback( + [Output('dict_data-list-table', 'data', allow_duplicate=True), + Output('dict_data-list-table', 'pagination', allow_duplicate=True), + Output('dict_data-list-table', 'key'), + Output('dict_data-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dict_data-search', 'nClicks'), + Input('dict_data-list-table', 'pagination'), + Input('dict_data-operations-store', 'data')], + [State('dict_data-dict_type-select', 'value'), + State('dict_data-dict_label-input', 'value'), + State('dict_data-status-select', 'value'), + State('dict_data-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_dict_data_table_data(search_click, pagination, operations, dict_type, dict_label, status_select, button_perms): + + query_params = dict( + dict_type=dict_type, + dict_label=dict_label, + status=status_select, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + dict_type=dict_type, + dict_label=dict_label, + status=status_select, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_dict_data_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_code']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_data-dict_type-select', 'value', allow_duplicate=True), + Output('dict_data-dict_label-input', 'value'), + Output('dict_data-status-select', 'value'), + Output('dict_data-operations-store', 'data')], + Input('dict_data-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_dict_data_query_params(reset_click): + if reset_click: + return [None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('dict_data-edit', 'disabled'), + Output('dict_data-delete', 'disabled')], + Input('dict_data-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_dict_data_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('dict_data-modal', 'visible', allow_duplicate=True), + Output('dict_data-modal', 'title'), + Output('dict_data-dict_type', 'value'), + Output('dict_data-dict_label', 'value'), + Output('dict_data-dict_value', 'value'), + Output('dict_data-css_class', 'value'), + Output('dict_data-dict_sort', 'value'), + Output('dict_data-list_class', 'value'), + Output('dict_data-status', 'value'), + Output('dict_data-remark', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('dict_data-add', 'nClicks'), + Output('dict_data-edit', 'nClicks'), + Output('dict_data-edit-id-store', 'data'), + Output('dict_data-operations-store-bk', 'data')], + [Input('dict_data-add', 'nClicks'), + Input('dict_data-edit', 'nClicks'), + Input('dict_data-list-table', 'nClicksButton')], + [State('dict_data-list-table', 'selectedRowKeys'), + State('dict_data-list-table', 'clickedContent'), + State('dict_data-list-table', 'recentlyButtonClickedRow'), + State('dict_data-dict_type-select', 'value')], + prevent_initial_call=True +) +def add_edit_dict_data_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, + recently_button_clicked_row, dict_type_select): + if add_click or edit_click or button_click: + if add_click: + return [ + True, + '新增字典数据', + dict_type_select, + None, + None, + None, + 0, + 'default', + '0', + None, + {'timestamp': time.time()}, + None, + None, + None, + {'type': 'add'} + ] + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + dict_code = int(','.join(selected_row_keys)) + else: + dict_code = int(recently_button_clicked_row['key']) + dict_data_info_res = get_dict_data_detail_api(dict_code=dict_code) + if dict_data_info_res['code'] == 200: + dict_data_info = dict_data_info_res['data'] + return [ + True, + '编辑字典数据', + dict_data_info.get('dict_type'), + dict_data_info.get('dict_label'), + dict_data_info.get('dict_value'), + dict_data_info.get('css_class'), + dict_data_info.get('dict_sort'), + dict_data_info.get('list_class'), + dict_data_info.get('status'), + dict_data_info.get('remark'), + {'timestamp': time.time()}, + None, + None, + dict_data_info if dict_data_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None, None, None] + + return [dash.no_update] * 11 + [None, None, None, None] + + +@app.callback( + [Output('dict_data-dict_label-form-item', 'validateStatus'), + Output('dict_data-dict_value-form-item', 'validateStatus'), + Output('dict_data-dict_sort-form-item', 'validateStatus'), + Output('dict_data-dict_label-form-item', 'help'), + Output('dict_data-dict_value-form-item', 'help'), + Output('dict_data-dict_sort-form-item', 'help'), + Output('dict_data-modal', 'visible'), + Output('dict_data-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_data-modal', 'okCounts'), + [State('dict_data-operations-store-bk', 'data'), + State('dict_data-edit-id-store', 'data'), + State('dict_data-dict_type', 'value'), + State('dict_data-dict_label', 'value'), + State('dict_data-dict_value', 'value'), + State('dict_data-css_class', 'value'), + State('dict_data-dict_sort', 'value'), + State('dict_data-list_class', 'value'), + State('dict_data-status', 'value'), + State('dict_data-remark', 'value')], + prevent_initial_call=True +) +def dict_data_confirm(confirm_trigger, operation_type, cur_post_info, dict_type, dict_label, dict_value, css_class, dict_sort, list_class, status, remark): + if confirm_trigger: + if all([dict_label, dict_value, dict_sort]): + params_add = dict(dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) + params_edit = dict(dict_code=cur_post_info.get('dict_code') if cur_post_info else None, dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_dict_data_api(params_add) + if operation_type == 'edit': + api_res = edit_dict_data_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if dict_label else 'error', + None if dict_value else 'error', + None if dict_sort else 'error', + None if dict_label else '请输入数据标签!', + None if dict_value else '请输入数据键值!', + None if dict_sort else '请输入显示排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('dict_data-delete-text', 'children'), + Output('dict_data-delete-confirm-modal', 'visible'), + Output('dict_data-delete-ids-store', 'data')], + [Input('dict_data-delete', 'nClicks'), + Input('dict_data-list-table', 'nClicksButton')], + [State('dict_data-list-table', 'selectedRowKeys'), + State('dict_data-list-table', 'clickedContent'), + State('dict_data-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dict_data_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'dict_data-delete': + dict_codes = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + dict_codes = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除字典编码为{dict_codes}的数据?', + True, + {'dict_codes': dict_codes} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_data-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_data-delete-confirm-modal', 'okCounts'), + State('dict_data-delete-ids-store', 'data'), + prevent_initial_call=True +) +def dict_data_delete_confirm(delete_confirm, dict_codes_data): + if delete_confirm: + + params = dict_codes_data + delete_button_info = delete_dict_data_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index f9ad59f..096faad 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -62,10 +62,14 @@ def render_store_container(): # 字典管理模块操作类型存储容器 dcc.Store(id='dict_type-operations-store'), dcc.Store(id='dict_type-operations-store-bk'), + dcc.Store(id='dict_data-operations-store'), + dcc.Store(id='dict_data-operations-store-bk'), # 字典管理模块修改操作行key存储容器 dcc.Store(id='dict_type-edit-id-store'), + dcc.Store(id='dict_data-edit-id-store'), # 字典管理模块删除操作行key存储容器 dcc.Store(id='dict_type-delete-ids-store'), + dcc.Store(id='dict_data-delete-ids-store'), # 操作日志管理模块操作类型存储容器 dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index fd264a0..4a8f4e7 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -1,7 +1,8 @@ from dash import dcc, html import feffery_antd_components as fac -import callbacks.system_c.dict_c +import callbacks.system_c.dict_c.dict_c +from . import dict_data from api.dict import get_dict_type_list_api @@ -24,12 +25,10 @@ def render(button_perms): else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dict_id']) - item['dict_type'] = [ - { - 'content': item['dict_type'], - 'type': 'link', - } - ] + item['dict_type'] = { + 'content': item['dict_type'], + 'type': 'link', + } item['operation'] = [ { 'content': '修改', @@ -457,4 +456,15 @@ def render(button_perms): renderFooter=True, centered=True ), + + # 字典数据modal + fac.AntdModal( + dict_data.render(button_perms), + id='dict_type_to_dict_data-modal', + mask=False, + maskClosable=False, + width=1200, + renderFooter=True, + okClickClose=False + ) ] diff --git a/dash-fastapi-frontend/views/system/dict/dict_data.py b/dash-fastapi-frontend/views/system/dict/dict_data.py new file mode 100644 index 0000000..f959c54 --- /dev/null +++ b/dash-fastapi-frontend/views/system/dict/dict_data.py @@ -0,0 +1,504 @@ +from dash import dcc, html +import feffery_antd_components as fac + +import callbacks.system_c.dict_c.dict_data_c + + +def render(button_perms): + + return [ + dcc.Store(id='dict_data-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-dict_type-select', + placeholder='字典名称', + options=[], + allowClear=False, + style={ + 'width': 240 + } + ), + label='字典名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_label-input', + placeholder='请输入字典标签', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典标签', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-status-select', + placeholder='数据状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dict_data-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dict_data-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='system:dict:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dict_data-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + ], + hidden='system:dict:add' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='dict_data-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + ], + hidden='system:dict:edit' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='dict_data-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='system:dict:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='dict_data-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='system:dict:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dict_data-list-table', + data=[], + columns=[ + { + 'dataIndex': 'dict_code', + 'title': '字典编码', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_label', + 'title': '字典标签', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_value', + 'title': '字典键值', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_sort', + 'title': '字典排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'remark', + 'title': '备注', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': 10, + 'current': 1, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': 0 + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑字典数据表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_type', + placeholder='请输入字典类型', + disabled=True, + style={ + 'width': 350 + } + ), + label='字典类型', + id='dict_data-dict_type-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_label', + placeholder='请输入数据标签', + allowClear=True, + style={ + 'width': 350 + } + ), + label='数据标签', + required=True, + id='dict_data-dict_label-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_value', + placeholder='请输入数据键值', + allowClear=True, + style={ + 'width': 350 + } + ), + label='数据键值', + required=True, + id='dict_data-dict_value-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-css_class', + placeholder='请输入样式属性', + allowClear=True, + style={ + 'width': 350 + } + ), + label='样式属性', + id='dict_data-css_class-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInputNumber( + id='dict_data-dict_sort', + defaultValue=0, + min=0, + style={ + 'width': 350 + } + ), + label='显示排序', + required=True, + id='dict_data-dict_sort-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-list_class', + placeholder='回显样式', + options=[ + { + 'label': '默认', + 'value': 'default' + }, + { + 'label': '主要', + 'value': 'primary' + }, + { + 'label': '成功', + 'value': 'success' + }, + { + 'label': '信息', + 'value': 'info' + }, + { + 'label': '警告', + 'value': 'warning' + }, + { + 'label': '危险', + 'value': 'danger' + } + ], + style={ + 'width': 350 + } + ), + label='回显样式', + id='dict_data-list_class-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dict_data-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='状态', + id='dict_data-status-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='dict_data-remark-form-item' + ), + span=24 + ), + ] + ), + ], + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ) + ], + id='dict_data-modal', + mask=False, + maskClosable=False, + width=580, + renderFooter=True, + okClickClose=False + ), + + # 删除字典数据二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='dict_data-delete-text'), + id='dict_data-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True + ), + ] -- Gitee From 6faffea65f26103a90d739509aa3fa18e52cf5cc Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 10:44:36 +0800 Subject: [PATCH 10/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E8=B5=84=E6=96=99=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88?= =?UTF-8?q?=E5=BC=95=E5=85=A5cropper.js=E5=AE=9E=E7=8E=B0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=A3=81=E5=89=AA=EF=BC=89=EF=BC=8C=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=80=9A=E7=94=A8=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8F=8A=E4=B8=8B=E8=BD=BD=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 3 + .../caches/avatar/ry_avatar.jpeg | Bin 0 -> 216574 bytes dash-fastapi-backend/config/env.py | 11 ++ .../module_admin/annotation/log_annotation.py | 1 - .../controller/common_controller.py | 40 ++++ .../controller/login_controller.py | 2 +- .../controller/user_controller.py | 77 ++++++++ .../module_admin/entity/vo/user_vo.py | 7 + .../module_admin/service/common_service.py | 14 ++ .../module_admin/service/user_service.py | 25 ++- dash-fastapi-frontend/api/user.py | 15 ++ dash-fastapi-frontend/app.py | 11 +- .../assets/css/cropper.min.css | 9 + .../assets/js/cropper.min.js | 10 + .../callbacks/layout_c/head_c.py | 9 +- .../callbacks/layout_c/index_c.py | 15 +- .../callbacks/monitor_c/logininfor_c.py | 3 +- .../callbacks/monitor_c/operlog_c.py | 3 +- .../callbacks/system_c/dict_c/dict_c.py | 3 +- .../callbacks/system_c/dict_c/dict_data_c.py | 10 +- .../callbacks/system_c/post_c.py | 3 +- .../callbacks/system_c/role_c.py | 3 +- .../system_c/user_c/profile_c/avatar_c.py | 154 +++++++++++++++ .../system_c/user_c/profile_c/reset_pwd_c.py | 91 +++++++++ .../system_c/user_c/profile_c/user_info_c.py | 79 ++++++++ .../callbacks/system_c/{ => user_c}/user_c.py | 3 +- dash-fastapi-frontend/config/global_config.py | 3 + .../views/layout/__init__.py | 59 ++---- .../views/layout/components/content.py | 5 +- .../views/layout/components/head.py | 18 +- .../views/system/user/__init__.py | 2 +- .../views/system/user/profile/__init__.py | 182 ++++++++++++++++++ .../views/system/user/profile/reset_pwd.py | 66 +++++++ .../views/system/user/profile/user_avatar.py | 177 +++++++++++++++++ .../views/system/user/profile/user_info.py | 84 ++++++++ 35 files changed, 1110 insertions(+), 87 deletions(-) create mode 100644 dash-fastapi-backend/caches/avatar/ry_avatar.jpeg create mode 100644 dash-fastapi-backend/module_admin/controller/common_controller.py create mode 100644 dash-fastapi-backend/module_admin/service/common_service.py create mode 100644 dash-fastapi-frontend/assets/css/cropper.min.css create mode 100644 dash-fastapi-frontend/assets/js/cropper.min.js create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py rename dash-fastapi-frontend/callbacks/system_c/{ => user_c}/user_c.py (99%) create mode 100644 dash-fastapi-frontend/views/system/user/profile/__init__.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/reset_pwd.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/user_avatar.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/user_info.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 246d710..6fd8a88 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -13,6 +13,7 @@ from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from module_admin.controller.dict_controller import dictController from module_admin.controller.log_controller import logController +from module_admin.controller.common_controller import commonController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -22,6 +23,7 @@ app = FastAPI() # 前端页面url origins = [ "http://localhost:8088", + "http://127.0.0.1:8088", ] # 后台api允许跨域 @@ -79,6 +81,7 @@ app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) app.include_router(dictController, prefix="/system", tags=['system/dict']) app.include_router(logController, prefix="/system", tags=['system/log']) +app.include_router(commonController, prefix="/common", tags=['common']) if __name__ == '__main__': diff --git a/dash-fastapi-backend/caches/avatar/ry_avatar.jpeg b/dash-fastapi-backend/caches/avatar/ry_avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7a683af3b9fceb35b19a74c94ce5da212b2b89e9 GIT binary patch literal 216574 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

XMAFL2 z0W!A4Twj!Rlh+<$woNqn^Ej%4l^}*5M(SHQ`V+&57!rViu5%R{BHvmVmWaz zXS+GGd6J5r%jq9ko2i8B>JNdP0X{fmAsUOr3Xc6@yv#Ryvg%5Y^u|*%cu>=717G~V z3;E!434#!)$`d=jeclWu0?(gxf`n|IM!+dzfHt&fw9~emc-lnszmGf)U{tX^Oi7k$ zLJ)y0NDyO3vgugvQw7iIDrF)7>tit-`s0bEn1TeQ?2#s1P>ewuntAIAx-FfmN>th# zLJ~j#Apm*BjlJ=FcY}j4M3m8KhTWl@TP5 z^){anE-^e|8ATqiU)F=(D>j+ImSCWiC~HnP;YC+vklA&0GH6V21wyFj1J+FdA@?0LXwkh#ofO;e0kASxD}N6saf<0j*X0%J9H#n}y11 z&IE-_*n=j-K=PTj^wh>6&mpB*y-zkz(Tm>?#Ji8vF9%Isp{&W+aYEML&>bf7lI7KC_^P( z!{I3d!bSuon1unOiKZgOR;Gu}7|rCUQc*nYz(O2V$iE6njz&715=<@P0Z?Ws!`160NO;8f*N<*XH{ZKZhHUwYjJ%OeNAH8$pE=$IMRQqufsCc+7F$XU!c2%D zLAW9(_Kil(;9^58+mlo)I@BRcuSPogUx;|-^wa&--l8Sat9M-~NP?M_L#|cSyrQzF z-BX9WXT3A0-TnJri)Aulpp?o$EJX_=Sb82W=H4f^985wPASelx4JJA1 z@SG4NqyPomm4Po}%e2q+3F=-RkT>wv(-y^zRhLqW)1@VGil*k zh@Q*=2{Rs})I^M4?3j}jIhB0d%9+14&21YyOc8>qHufrqu@f&L%IH+`xX|BXQ_yge zrH!xXLFa8Q@UbUZYUDNY2)ZJkaYc`_fMzXq4NS6A*)~ z2alzvl$(qj5-vTPlvPcH0YI5m*PVN8>PIlm;rD=9vE)Q_*T?6JY%&nIwq5@KeO`G! z);;`JsD!3toJ&d^MhTdk+raA<@Q;kSNI>e#Rc`(wqaO^qU#Q@1ll!w{v=!u(sM<*u z^`9?a4p^6iFeX)Ws=sQVF;9&ugnNxSweQ17K-X4;{+pEupR@1R(-!z-peai`ghdUn z_Z2wO2g}XTIH82%rDV>kGJW$O53VNRg6v%pi`J1N{oOJ`1Il@-cuMM>j$z_Nc*xt(;F^ye7h(7H8ZOtSv~yHElmYSbq_&_!zI%1~Rx zk`z{EPdj-|EAfaedJaauao)@TCtc;?T0Iw>HyMhFVRIj?KLsD(71+1`0ITQLI-a0u z-=sIK+O7GY*Xz&Yfd(UpgW+5Lk?k3WRGEdSCKafS70h}5NIUtH*B?biK?e5*6I(oKpwHCzc5cTl0_SiU47%ZLX^xA)EMgr*+C6glNToAr#^h~TRiSf%9LE}Z$r#Rhr@7`NC-B3v`uZS$SA7vk`$CFDM2Q{ z=p^(Whr^zaEbK~^LZMxMvfOJ}yv5nn1`g~!hYGi(Rd2K&$K}<6wKVL7fR&Yn0XmaB zkHCShm=5&W_uorIDYifXDMcyAN|gSLQ}*=s1q&~xm4KB8l_Vfc$-IGWM0m~`X{I2( zL9oi-tLqf_@|kfoR4&9SJ&}sTQ-u*5CFD>PcKeK6dP2jGT+LiBErMi+f#B+dT4uk`u&IBwpHc`Ne?7lV_WDR5V3Fo_VPiviSmU#P@h@ zsM`-$GL=Vp)TEODK~NW)0ex|0Ee8P*V%>K&?0a9lP0ca9t&Z9}zPm7`ed=ocoh~#g zf>}vd5>ge$s}AYmf^^p$;!c38s4K#h)`A>VaUQVKoU=CBJj$wvGNpQ*bk8l98g&Xv zkmGa46Ja98U>i*I#eBp|gE1rq6)0$4qN`P+u2ImG&5bHD6HLe%6*oS&iEZquK)m;D zDC=F7Qqw7F;UnS)K^n#6>5MOk;)6CQ$V`07L)>Kh=Dc|C!hHJoO!O%{{V?h=6(f9L7jtXc=o&rQwc>#1CU1N6-B*< zXtGx79d#|5k__=dHckAm@dHe5yd(Y|8s%P8>V}CJ&mH`x{{W==L@mzqRaX}R2@*$i zT_gQW_3NfJ_)>%h%+^vnVg32WFiFomvpIGsrQjViL8er-caINvk*W3RriMNkvXWK= z*%Ouf=@3m10|{!1#hR5483|bc`s=2Gei0ZXLYieQPJVV{35}@EQ%O##R|=!A-;NXx zX@s@Of4sZW7);YMQ_6A9E+NEu_|KP**usdX6CuT6Fy?4gDN=5F#ivpRx^wX7%|PbF z3PCBO@#oqk7Clokc&!&R)6PBM9}D2pej}f$h6EZycBy=BgEJAXpI(=XV6uK(LV8DH zVi`&hnv9X)LuxQ2+lQI*!KRfe0TzN_B9+J|82KQnN=USs2dSTjiS-zRjUh5kn3T7k zkth@^NReL7yIgj2dRr{oMqj&e}N@Sd9;&q6Jl;{@i@eIrUo?lGw#YjdIQ}x zIgI0H{A+9&drlI}i!h~16rf8UK2iD@do5Nsn{MM{b9UBUH5q*zk>0mtFCst@5Jiuz zywe*47EF195;u#VSA5hj1f+c-;afzwyDWuiD04(g)Sl9@GetKVQdK?SCe!MEk*=4- zt;w?;@DU>yA0jl<`bS<#Sn+g-;j$7(IUJ3m51BW##unjSG~)x49EUHGaTjvW1@2I_ zBz7$spYXRh<)_mf95Hp~pAjScK+GPV#6eT}Jv)H$Q>JhFCr=U9Zx{sFUWKdI-l7dA z(<-9UkLIYnJ)=u`4Ims{f zT4SZU;${@7V8J=(PmF~loNcHhITY#JrH*i^Ya?|Kkbsb@^ni8mYCLDi;FAsEt*!cs zoK2{eQ&vB}Iv;oqS(m#>Bx%YAPDkB;OJFev6BH6$1CcDMRNKC2Nm7$if{qdhH@2d0 z@{e{DaV{c}Ma(TmlC7`WL^(Bzq%vgV>k@ok?zr;@ZzaLO+X{3T;Tyx zk+#A+HjomoA~c?+V_SQt1Dc$|@cz<~X^IeuvLN}P9*||0<<2OCrMS22w4Rsyy}Dzl z=YCxLkHjcWM$52WOp(n%DEZv=!VW;p!-?fk_ZB~|Q38{Z(heRJz!tIx*zt?{;#TC@ zd5AaEmt5^k>l=ngO~PRS^0Hz)y8I*82+1-LrMq94Us6`8)KjQ<47{n9!Gxq~c=F_A z{mvlIvdvHtG(bm_>Qeec{Zam-!B2H)EPkA$i-2fj`PWY$p|HMzRYIdQRBzv2y#f>! z4nvbygjQ(21t`2gN=>BN-%gPyTWg5`f4V9FwU+lZR^aRc!7(xA6IiFEo|kpcghT!P zX9AZX%_+p zY%Qxf-uSkGzSpz~t9dC&Ab_6HN%(WTTf-iS;CLkNtGKqSs%V;wqr^Xwc$k`bfFym% zQXS3tC!$s-A@x)7ww0b4v}^K>d`xl^k3z!00!c^~s0#da(`9u35i8SIbPhYp8%sC0{}^xpI;BBuUrOc0aAf( z!T$h%5d#dVU-^S&NKjBbqy(i(x&%S7KK}q;e~u!YJfFzh;3o|t&OqZK@X&eNhan({ zDIEU*eqEM^lFAm5jN7*SF*Lx;0xJy#&yh@$ltXVve|aWb zNmAQ&DO@CpD;&Yo*WciAYlnw0FDfN6Ox$0c*v5As#HE-t(yhv&SFPEijlHV(uTQkb ztTcG!xdbTcNsmtgJh5RQK1X=b4HctXhdtx=A8Wl=AZzj&Y4o)&BsP^gkN$@{1jzOsU^aLd%sIs@YBCoWZt` zYFy*t7Am%!@9KV9$3qkD4pDM!$jX9=z(oc=6w1LumH>`Bc&iS%w)izf_HsY-UxC6gI$EAb?<*m1%8$c<2)= zrj8mW$@q?brX~lo&gQes<2`P?v%-Wer$=>P;zGG}BhTZ2V$J|RaBylxLM2QZNhT$C zOD(kFmwEJaxC*-MlF71`Tw0Q*nrIo3{sYoE5_}@~sl=U0Lvs1#hOwmZqxgrfD-)3?Hb8-=M=jR zASY2WCdZ#6;g0rs0GZ7LF!J~IfY12S{WbRI5&r-`1xsG4@11X-Ie>aVN_fF0|Wi}cjp$w5vdx>6>l;rFw)+rM?C zZ2r|%P-Td4E~_m{lo)YAK#-sa1t#G21mX@pAbKHUr3EUv4qlMs0&`(nOj1-rjT$7KoW(zOgU1h5ya)mo0&F@Rc(7jl3>g?r*toOG zqy->4^-9yp;_yZCOBju2(qOWoK;_;EYTu2v9$jHmD!rN&=92xMHx&OQArSE zo&El^lgBHx{i#}1s}^kEgQR*?k{NJ`GLQz83v%OAz`j(Uyv)+8I?SrgNh)pCyaE+? zf?@{0eEIJDB!Pv8{Zt_2Zx|09no~X?69Us;Iz)eSENu#M;1G2%C#U-sxbeoj3ldfe z9Rl*FQ=#P^bBuoTlc&b0!~23f!T!V+k*GI_(hrPs_*=%$4rNS;ZaI0FgEbEi>&_FE zrM4;7`4XgpvpP%}^ZVBXV|!<8{{V4cF(48<cj6X248a%U>zO zz$Q|+D0TAohV|;@FoKezv(DXox%3vlJR~_pcq(!JGE@NuIonJmHAc<_G=wQu^MGnu zR=&PJeK2W%5zo}eE)BuxU?R~j9)|uC(0nb=-GWF!m=i&t2r!^rpi&b7G9f-T6BG0F z<%v_xDFCRhVad~|&2<~ng%F|HJcY?U2Mouik&4ZtNoT;AAfo2ToNo3i>3dC!5^No(421wp9@-r! z=a&!A)9}99#W6ZLdRIx-a)|&pK z=s)8-Z`IkPedUxWR#X=aW29Q^REr?+CyR%j{sZsjig?rF5LF0rz3S=uQyvOcuE7L! zBp;ph_l{ifs9(OZ#H1nnf@PajIQ7w*F;-o8D3xdrE9iCC{ez|)l`G|xF`TAHF_uX2 zrCD%9%1@~gKBuoifXy**7;^`+9DPaIrsTf?dB>@tiS7Z^$Inl>#Ci9LP14I_9Zfmc zfL{Hha+1X_ee{Bm2_*98KeWNN9zipW(v>AC8Q3P;mW`&E0fEYYaQbWfGX;Gr=y^SWgc+NTVY68k}NHMe!qQiNx(v7MyeN6 zqJ-)>BvoR|JCr%At5=_2j4JplT2~Pk1cN;Eu&^5HF%=F+p@;@)&U}IMye0bp$y${J zYbIb3^wx05ayRm?OTn|ykWb~c8!Who@DUYj!7?cF$7B`}ewt-1XtPZD5b%Ev2rnsLF zPSR++EWTj&@v$wmAs`+o>E=G~@xq1vB$HEA?d!>49aSPgo)H>y^EW@Y?}AM%wNM+c zdVg|c#iPEpb0;3iho?0DD`_GTOGYLQ=?V%RRCCf8ZXjhu>q9ik0X08^T zC|3@rluQ|(LLgh6hmIl4vdE~@I_qRK8n$(anE123c!E@So|!aPXSD9nrzYLD*4Dnh z5Z+r>LWuwZ7~hR1a%iY;(nU-q1jtDS1uv$!l75SmDomS=zI` zzR&YjEV@-(_W>|u7ZiT{`JH_ez<~x~x~h=etdQIn6yWWo zfO!d#F?03%1JH1YG68S`jFsaUcA1a@rFr#aK-P_^P+ppYpfmUWyfNEQrw*SZ$|B=p zOi=-=i*a6cIfxdN!3Ts#QXoa9_MeZI0zd?SR72q@4#us)ll%1%xkW@Lv(yb|r!Hr& z@G-{hsED4uVr-bGj(~L>bhixdHjT%7M5RfPND1|~Tk^jw25D*w>+kIoF!6ri6aq-f z+*TeuwV(ToQ!-QT5zg9u+e|^2B$T+VHXPctN z%foR5k_4Cr44@VZmAC~Sc7c%8p&6pow-lzGY!MfNqiN;v9dN%%$&ATGsnM`g5Ij9uzYlF^^1%oSEH zDV9=5H}i--XWifjOiGo2u?b%;{oojwfyp!#xyre4u52N{tW&P7At0HH!6qj5JwIzk zB*=dg!o8aHPLUAJEb6D3N8qq!m1H#)@1+TCYD$VzA+kW?F*;fSl4oCbB*}~=AgX|c z4Mz&}<};2)$T3R=-a$d4QoJX06LvOca`SnNbq?!--po z?IcP4Nx2cw;8^6L)lXC3TSbltc?N##7pLs^x=I=xo}KpS+6!@%_qYHgz&$jX`J7T? zOiHP#VP-w|OPnyW5S9tW(kB*QvuVpI0+bMflqZ28001325$@-R$}=5e%V3vqu67i5 zqYj$s4R+jCKTM(!r6}kCRHO|qc}G1jhZ6-3DlRjNlQ1%uM!9qrtTI*4y7*sJl1T=A z_k%qrUtAL#7ElT0#C-ZvusJ2(YiO4&iRE*~8;zz&MIzCnlrI2UM38UP>8FM&F?O*b z{DFGt6~C`|G}{FDl9{(?;X{-S+j>M}+QOz%@l2T{X>M~dt%pBIz;QPH>1xhVUTdu_$6P?Ei^VI!9zLa8ZjJWF|e zAjSF)y+=(nIPV{9+?0|)N>q7U*E+=i01xoPw9WtoxO7qIls@MumwUh21iMX4+l>w? zgatVQ_lTPvBr6{jHc7#lHyLX%oRxwRj_~6u4B}^FZg)C z!WUYv@xhaFOzaFCw#S`2|C0|S&3;TR4MH`d3Oi|15#$hbuG zj{IA~@b+Djeb5w0T@W*hoETE#0p!92H%ot?z=YK5YfGe00exlRQ-Q~|0vXq!{{YFa?+z)wn z_JnG|cr9v)Czbtu+UtvbpAhbaDC8*Q9X(q(tepWiAE<3M_dZTlLgOi4_c%Zog9H)R zry~*Y#JMZT^QAj60%??`f$d^1KU%7XUVUmMOC+R|pudkTXVTa#3UYxie^umQrDUwB zMinFx^}W7$kr1QYD0yl)k5~_`6dpc{0my0T{)QAN_?g!8g$h1OMA!~~6KRYna8QCO z2t6Q3O4GteB9k*~8IcCxD2)KbY(TRX1&tHY!Rife(QAQ=h)ki%5EOc6=g%(EzXDz8 z{{U6p0NJXl-a4$E*28V7W>VJ_BmpukBH(lujCt>m;2rTYFnSABLLWVHk7xcLv`;P& z1wx9hXy~S+`lNKxm8}Jq)>XyYATH-puMs_Y_;_PA8;P+d{{SqN>L}fZD8Bu^1_bg!!K}Bpmu$^!#TBEV$ws5vab52At0Si ztUSEu&>Se0ETX_9s$VoI-c1dL&%0S)oCOr9qmJ71G3)%s6mg156Pe*adQe20+3He` z1i4_LJq+8q`Q!iZRM5=l$h&5ASs08o;x zYlJA=re@wc{{EtTR21QmK>-4bB6afgh~c+kAJi%e6~anarJ*5}Kv6u*e? zzR{p_N(~SmomwPT%k4MkLVHEVg5-MZ)2w+;3nK3TF3}!v46_0hA#CBq3PYf+DoTXf z-^6@x7@HM8Oa5Om^B5@sJ>XJibQ(^BPn<=xwiGzSD@5n}>z_Cx%wB>LJD>s}d;$HZ z9L@NYDgDH`B`T@IpLfm?*{xzw(MVnSo*@x^Nz&6G>Fa>R$wdbRO)J@m-M4x42O4cc z1?1oNI^1xq>r5HyrP`H6Aj=eWcLlUiA_RSe=y7H zQqW6~wXMV{t`G?dCRL!n`E&x;!Zt~_fkvW`&z71SIm$fK`6e#vx*#d1VyK9W1aYZY z^OY3^OInue&X*ijD(66eX~31Ss`CS=MJ74bQlM9r}LbEi}8i^Uu8_22`Xg^#k!fy*YdE(Zm}i z)7g~Uo~Kjg&+n!bDBn)#NFClHK^NuJ^NX8dLW8`yvRz8bL?^sCfJ_sA4_UrBpYc4s zL>@%~Q_tca{GypvSz?_?2eL>KJ#ROe1Jf1wrlRV7c=W9z{{Ut?*hJMvV=V33!fEF? zR$EE$1uEl_m_2kc@$kefOd~qd0Qo=$zv$n;zav7ZF3Zv{{Zyr z!!H`7C6&mNBnx!879Kq0V=Ll&H{8Kq+}5jFYZsdiIFx{ER94E0zPd%;Y;Ar)Nk>fP zZqj>2)3mg#h&Mn!>(9j8iOOx8Fz-ODd}im8F^e-$s?2oY=kJ zZyvs?j;}p$BXw;l6sTQ7T4nOFz?G|wNlKuQU>PxSh`3S_iV0WcT2{(~$_bB(C&QaD zDXWpj$a4M0d))R0-UdaKzqNZanA2@$P6Fx+Gzk?I4X}i%Yg$=nw01ZGh#?WrrZu=B za77gY^2@QUqs_c;2z^PX16Iu{Qzg@!RQ~i_uG!~Z%Btw6~Pb5>YB8?k<%r-`+cs`@HDb+MTzPKN`UWg#dFd3;09w1Qr0)e2_U)u_;| z$+sxF*AvG`-wlrvfh61xf;BLFvDeho&N-H0i!e$9#H%XssfV*8+`Cz75VmPp5IgdY zdY@20k<@g@Gfq&T6wLm+;~Gy7+1GG^b$7VQc_Je5hX*R%vdu~xZ7BgnNJDXQMWhR# z_km$zP6!TFZdhlSQKKwsB*6vd7C;kEF#fs3iP-(ajKGH9Xr3?$mhIi*Z zRVoSOsaB^wO(JF<$t=K1M4(Ju(=o{dWkgtse~LcjX?8!@KYn{c+h`6@f7zD_43w0L zz47k$gu>zkijppF2_AM^0iXO<5G%X))Sv$V2R#>DmMi>s!f-Zk3fi`bNZ*s_A_QIS zqXt6i?KV*ow!XW9wz1Vjm1txhe|EpFH6K=W4a(<(e87yg?*TSyZL;w`mB+^VBxj5Db2yAjzIlss4O17iz{L zQiW6&%GX4$dgl^$Kh9m|0E)8JKdZuHIj==sGE>=}B?%$~-q*Lo-VQQ*Ue;tYO}u~t zb@o~sfGlqwjo}^=cHgE35u1F<2@;YJlq-@i^dXYkY^>EFr?Fk`NllDQn*$Nj;`-u) z6UPW?h`6Y~Ka`I~*}wL0g^eg930N=KujE1NdqY_M#jW=W!(^nBV4He*z!UI1qfg^< z5i(**sDZ&W8j9bCGaqXI0NG5NvrR4mp+UJ)i0N{_wseNkY*%4S;Zq9|lO5YyW^Lhd z$|mCn@Xfwy@d4gYwY^uVs_|45!FZqk&(6*Y;F%Db%Ixa;H?C0+w}q50k>w;HiBW(? zp1Rvu>5ojz6r!iLH!o@I(v-ZB z2vSOdtWDz6=W-7|XB8eVk2I1_K|lcjnufR)%88U{KZ*E$9Q#CokMjzExyBZ?=)63ksx0@ixC(NGeBP{-Ea#lMd1^X%Aj z83W1+?uIGAGyc*n4`6nEq-AvVNJ>lnntQw_jN6ibQJ*g#0fENx3~k~!n3Ni?R`QMB z4}jodfRaiG%yHpMCpg_!%j?Zl+fAuXg+OT_lO)(_B2BOE=__Q!#Kj;GNK`5fJ(1|I zXC@mTWW&Ica7?nKr6@WfV%Buy23u#jJ+IGqdzfttyFZyGo}4YzzLs6!Qc{w5f`TB3 zyn}pfd^2U3&Wnmn%cTYa+ge~MAv|eM$qaTS@$(1#&JyH?b#X@rGWjLj9gw5o5&%Al! zb`BosaG?t&3qLEI)+=#8&35+oh?Tba>CEciJ*OLKR|u0L#&60a#?oT=l_aw$C{yN( z#?f0dD;v?1Tc;)I4bv`%N|DNJd3qD$Z;zP;M;_?_CZzLN&LYE=u_X>iq(_gikmPz8 zaZ$v2#)v%+xZ$Aj^d5)YaG^)gdLMDZg&SE1j9BPK6d=SLpxF$CrUr=;MwaRI1bXzq zh*^jv1y@u|vV8civ1_tpN>G_llv3@l0rPZ@f&Tyk-SDxqo7T-XFEXkLMsG@^NYks~ z0m^7_gozTOl?Vd%^PHEDVFC$2!kUoUUDq=4>tDroyfl_w;8Z105{q71iUMgJu5A8x zr}Fw0Niu|`9TbrlGGpT%bjAd+Le7rS=*C^(45*okbx3unt50Ow!2v*ZJhe74)6{t3 zi2xFMX9UB-m?1K&f(Xf=RaR-l$sAYf;62*UUxp&@o62vUI$H5-h6*AVDhgK3!;Rq+ zU6>^exbbkpXZ)i6{#Mf7K;gcoo`0w=;oblOK{m_v4A#+pP5h_N;i~$YdH$oV(pd|&3_80bMMY0ml_>!N z1nD|V&$>KxzB%8UKOylPPlOQ}=>GsJ;vVW!#HX=Pu<8Z1^ZAatVQ)>JVk@yQ+<@!v zaJ{S?Ey7t;D4$+O?_Uv)_syj~e;GsJU!00Xbc_UmDv**!Bsq>cbP zf?|4eo_vp=mK48;?(oQRJv+$4>nY4#qFf$R6-J>2_Ve>05NEf<3yt|)IvCUD&&uT z6@>n2pY~z`5N<4Hbsv1^ zTnG@M#YeB`^f4}3P%0JH*2$~pA&RWc#d%ufs3h?ctJudDodJ{O%MJv`X_izbRSs;Z z&1GP`oJ0+EW;^?c>(oMy0~9kN*HF$K4o!Ob8pw3GziyfmcPebIgSImg0mPz#9Ct ziPGLj9*3|>Fyvh*iEg1$7p`N(_RLd>IaEJ!bx=Z)!sMqw>Tv~1F(;YkISzV#J~%L* zUGrXtAzoghE4GPfg;-JOtDb#4#8bB}4mqBl>z|*;97I?Y`)dnGOrBQy+CKEcg$Hz0 z$_PrF0%GT&JpFaQ&k7VNX&dayN=yPi%zXT&1{k6j0?(R3r9Ws>>^z0uflx8Ra}_Fi5b|nemIp zDSSr^5?aED+&QXi0$H3K4>u>HgLm*+JNQ`6K_ytY)`Cum${YHXol z1c=~>f&rNpI^w25A;ttvDJyfXO`7pz6LAdkNjCJe0+n#rTc|}N(iA(h_T1k#=r)vg zJ55oVZ0cMMy_IGzs-d^3xPV(Jl2nuwaU^jnFU~Qi!$XHC1b!)1&M-bRkM~Ro52>}5 zpSRKvy{>(wH&5*gvw0sMmBt}Sh(yGe0Z~J8zfLzX zHe6#YpeZzEcB!wfkn?Pg?piBBG~8TZ5=aCdv&#BfG=r?*qH;qOX?GfCv^M@y=~&%-;p;$;*AgMk!1u+380LunuhC=zA>)N_qJ_Qr?$?Ma)oPi;n>$Z(F$2?w|Kjol3@jEEz;@fvGwF)IUS z`goGfx`?iVhEfyH^%cG|v`;^V?J^nE6N1~zOnf2&X3Omy_-*5e6;)zmzD@1S&&T15 zXz)m7vj-1q-vPG@bTiYP{{Y@4;Q_iC+4*+7VWy$reIOuNTc?gR`Djm`EF`Jn?jR6% zNU{=?i55N_uNr&#`V10k9tJs|@jSgmBXje2?|veDdEXFbsp5TNB$Yfp#5I)VYpgoY zYI^A>tjIqcF`?nz$Ef246&!oG^&Dby?hLM$x`Fo-U;w?kU#6Z1!uWx-eg}c}iySL( z@5%Uai-Fi2-KAyJec%W}Z#te41H{jlD`Jxno#FwiuK{(*JtIl76qFo-sik{fFy_CR zsm989fwitjSvES~Q-Jr|#JM)tLNY_|pIB`3^(|_7!lD!sNG3D@lhcsre-2n;6c7zZ zVv#OQs7%XNr?*O!hH|qvOKTuJqXuKjeh2F~hdv+sxryRQu@A-RsY}GX0%pXK>OmU1 zrjhkNa2!qC??@@M$R|trbm!9^cjNfqOX?r=iH?l_0H#l9`bYl&XBYz#Tqy_#c!1$* zg119jGSU=Ow27}z68K6;(gXn?FXl!(k8knEE?GtVuSKe*7Cy25J^WfsP1X-6G$zAF zzea&9WSM#w3Q81t01^@pUO>jaQhcaC+vZuGVv6-udo5m}1M-cgFYuXXlpVkU3VzV{ zDhv9BTf#?eG8$7t=}J;;sen3p^zw^x#`C~DYDt8$s*`2&9iE~*qxrA!oH;P(GRgn| z&+~5Eu#Ty%1`cH?RnD%c3+QuY-5eTg-G9VgdU%if~it`sN{_*^@+b9y6+9$%&DtF1CU-<<)rHz+q2T2QxtYABVB z?v)h*erKP=U>+U(UlVJb8KodyK`CINJIWyV&+@!2k9%@?NeYN*Ve0fcREh7fpNAgF zWqVImwfkQCY0Hbj)@c_U3L#ujLFFIIoyC3l!EuL9cS60f4skVFrta-PI{{T3| z#wI{ieaaQyj*02PVqoW+5xXI~?B8POvYf5PlSt!CwpeVWr-4b*FKgQ3_QscE@JTSp zCQuQU$f^}B+o6v>@xL3N8d=tIK_7~i^G!JoBaQFGb9Os7yN%#DGG<$%MMKZH^)qd7 zH^x*?AOY!ZbkJj2vtVK2$xy)o*D8XI&e7yvCADH=V$9VhnJT%lGM>$P&HqFvssBH%k<6Fok!3T1k=25!Uh3myRZ3giEOv zr%u)@F@Md=+9F!iR<)?Bh>~^EeEEYolNIdgMJWMX2?!-2An^&|5F?0BhmWol0&1Qo z);%@^^u&mdHBS@kA0`(beEcu|u*Qdhj%K!`datw$?EHQo&H)l*?$5{J{NX~6yFVX? z^MwjBOMT&feL7#q3K-DvFg28kJpTYlCj5+j{{T3TF+1cN)3wPM3)&`NP9(v4+ML%< zD3qSdThgiB2Kn|IYv`(%MAZ&^NSd{@t`YfKhY-uf*f5|{R6x8)(h0>!ieQ{cCMc|& z2ddYN5#v?-b8Z+olBpqPlnX1_P!-WR-}Eo`dEPZ1-EP}vv;3|90MFA^P@!pcY6>8@ zl(?B9NIWX%r-nS=ZNNBKi$_${A}fFuN6W{#Y&%5w`#unqixs)f-B}ByNqQXNYe13` zsQn>EE(d{+D-nou{wG4eyLx#15EClaNI&Y`HJm>Dx%n6F&wYa=w$LzO#gJkP@e+oEwx*lL(>!#6DM4^HUL%Z1<3gx{ek}bG)H9H!=rbjmDBR z#A&fXL=K++0M}aB7Y}DHfhp3YFz&9F3*p1peEDa^y}O{ZAN?Ylz@66t{lyrK7gzV28` zbL>(U;nEO$PrPS1RHjxS$+E8yd4AYY*V7r8Yk#j+XU_!jtfovPo?Sd+%i)I=97J@$ zX#W7XBij9;q1gS9+6>~k_SpWNL03{rmI8r8YEX_NCgKSk`Qq1OmT9piD5z4F3Bg8j zLE_s?m{?Od405JQYFU>hf_}B^v^4w|3 zB}+MOYt}PKf({e}OiZk)V7-dK_EW~{7M^kL=jr3F6ao%X$|P|aI)SPBdi?OAivn^G zaMDm)09prxe;aGxl@kNOttfKB-xFkC;*Q<3G*PLSqq`#Rua zy#Z?s<;4bT)w#k>vY@@2z7P4ltH(L( zKYtt#3~ux!yIV?kq!xd~>d?8_>KAC-Y4Tw%Rkr7k>Q6llhbsa#<%-?4Or@v^DET4a zi|+y1c4frKnSxx`ugkK5|G?@*Tn96Ixzb?eGMoDy0N9rt>~ydyawLCn;m9Q#jKJ*?rTX;E~?36nZV>+Zb$G2EY* zCyNO`9(|-6?B0IzD?hffP_j5lP_^{e-QyiFXwmo}BNAGmA?sA{+{4GS`896PRHP(! zouh~rl6%UIUVi;;7?Fk-bTg5{uC?bLx3LlQc`ckE1v3ZFR9Krkyxf&5jk5JxKf0u% zY$U{5d_Gvc*sxzPoCj#$R#@j~UEV4<8qj-uMB~_9fEi62C}l8!EMuqL{{YrCI2cJN zT)F0#rQ(+qnN=LGvikwTD%bx2XcB?uKM3z@Rj7^>f7mcENtyPU+{#Bjq*^&(n34e` z#4Wu>ky*B4%{I-52`G0NN)NtUQ_?xi-;Th&kyX}|p;jc=7>>8_$1&uFS%pCyLU;Y- zVk0`JUka|IBo3cTTK@p=6EMFK$ax0uIOb{|20m>`QS#(tUyu8YPmznnm;(NR!v(gB8T1g`I>VEHtMyHQ?dI*asB4Wfb^&Qm1V@hJS zdO7ve9Fl;TD5&BhdTwz@H2Hec08vQk;SqV(+>e$j_L+zTjYW}yE~mWW#|Ej8JpJby4*|uR zS*1yhUC4Ay^fg@-U&}w4PxUad5GsOM(UAFQiCC@;^wc<3@JO*)5fDJUkS5&xc}F~b z2mDtWV}>`bhPk~>d9OMji+}cogk>8^KWW{>MPg<~P9>4NnUmQDo_8~+Zk?sN>#m?7 zv?QrZDo}tD0VJ7#03^>oTCKYT>ZRw@CE((M+ z@0@#XC-PbFHen=~gYZIx1ZHPmYP6+#b5$P7ZXaM6aoT16UoB`CY7!C>o)pTXgq}8@1joYZ+EX-6Og6BAsTUt&Yvm;*z2a zEEP;28v{CHqq86{D+0GV^Yr9mlM@zPLThpu)T{OEp)pDi4$hjj@Q?~nd)5a-_wK-? zl?n?wo}pQIL_A4?u^)>D1|4APZw+OXuaQS=eh-amXOP%@n(zkcrsH$Fax#El26U>KnNP=Jv^Q&1Hw#PKwacjhN`2>_6` z5BvRyA58O%yvwa0?l$TjNCpg{VL}d~;>VEcMjL3t6G(|r*YbB`s`HCYlMrB;ClgSD zqy1S}`^R8kgGaTEF4$}Xd$G>56t|pYfErZ3?|P=4M}h!RlBVN@AtKN@>*wA#v+mAB zvfgDyKvyyGl8E;I0OFn~F>r9@n1xwF66}C;Ii(p3c`Dk9Sm6be$E{-ah*IMk^+mfK;>d zdn#NvNQpG6#kp}FzL12>0#>DQr0~tSO|`YT3;VDGIW%tX-f&zn228>2N_*;%ip+Cl zd0%qJmT0G?P{{TQ?_s&Dl<|NA^6)1bGWka>iKCpzmg(w14lC;PI4fTSBAOeHUKd(~^Fo_`q!juG~*SqwDo^0xaD~B|mejYmXpXUWo zXhl-DiFkNKlmPSevM+Xq`!i)#6w8MR7no2s_s*R8W0~dxS%@H%HxXlpv&~Jy5nKzO zZFq(z8s~E4A;qc0Cd(vo08d_HQ~XCPS51gXD1F!T)`E!)qK8t2`&il>X^nf76xP_48CIwy0zLADy1p_p(T39jjb9<^5+=pP)P_0e@E6M;$sP9oUlnrs+wir z2&-SjPuhWfnlEh-5-P)Ue^q;-v7gKWlpNk{~Z;cIaU zDz3{_X!6hGz9?mqalQcifl`GvKFyqRv6)u8m{nBQI!fNHsBza7#U$}6Zf0%>Kh8a0 z!}e)p!UT=RxVX^Jsmy>ViX*~2TWdJDv+qo*O$w+A?b4y3GE@E|-~PP^yAvZnizP@P zgz|On_R2F2sM_9=Z_Abx2XyYRK!x(ws|GBN1gBmh1jr)(f6qK9j*gY!@Me{%#PRb6 z50;kl<>%00LLku!p1h#Vmpe2W(rxR{!~<(#LrMq$A)xAC_W&GP+;Kt(5J1<$57~?c zWl~VpwLEo#N%=bQ%99$m55LX zDN39ifDlRak6E@M?RY@QPEXMVlnR|gnkq5j4dUskkn@Ns z3F5!$0>WT@{{T49Z1{jnEW6T#>ty5Uu6`@yz8x+mEX);2DL|$5PM-zUt+b>lApim( zo|@_7F&8uCiIZYVSWFV{-+py`Xl=XOD{1wpLVt zxJrmR^7r!e=YvJ>gi2*HvCyFgtm3DobBW!#9P5g{oiCv$iD>|su`5h!d0I5(t|Z{r zPCfPWk4@NSA4v&xEOOmVS$h_=-Hb5BJL*sgFk;$T)1|=ob+|f>mvml;2LxiC@Li+v z<vBFirn_Y**v!?R%B4v1UBu(qxF9tes~0wJbi?D&Nv&UfwI=D`Q9m)G*VMhQX5z3 zc>teN~V-wlU;UhoJ(WsLr2u_@-K0rAvNNrS1=1I3x9 zQh+FK^74fiFSwnMsjN;|mdS?}(2ytfEj+a6`(lG?#UPQb6u$$A&k8IVjdj zy48G+j~Cy#ZPynvls58zL$t)Fgm;RNBwLm3=sIIRkBE1Vs@W@A>+2hD55QSv2!#4s zD!s84mj`on>!gIzl&gkdAYZ2;Zc;54=^_)e^8NS5FQnN{^%1iDuS$BIkg7XTW*#*BJi*8N-@y zs0gn0b)r|MR*!K0D3ZyCWZixQyIvDnkF&)sM{%~(-kCfXf)10?be>xD^G&e+#8v%IseUHX4XTp#P{{YGvYi0Bn3lRSRc>B7|Hg=KvdfIg<32ENV zX-(A=as){|Ez`;;hj@M_4m7`MA99hviy>F^xr$F5#TfF=0#Lf>MpZwksy+1OwH>SJ z4JatMpgV*f86qYmTHb!)?q9~_#rk+siDn;;c>zr-SeW?#0RI4LI1-8R0jb^;P!^f0 zFE_M4KEU40yJx)p<>Yz8IV+WU9$%SLIZ|9{PPVX>q`=}q78>-&q5V8k2ii&oL6b1& zUo{cpejP8-BA2=-_2H|>XIb9)H(BhxxrXYqOumOF&T{Oj4JEPJGf=veQ-n3y$U@_^ z1L1>7g^0k4nvAJbS~D^riMwZtj5!G2MnI+VPEM8U72j9*gI8}KcH3`HoMnZ*opC0b zpEZ7=3R`BOw-BWvAw-DckU*J?S#0|;69knq2;nsT>z#7g{v|BJZ@Ftj>ra)<%m(&E z-6rj}YKo_OuGc-A*VQfhwqC4zj-8gBXu1;GNrMppKkkPfT?#Az0Be4NDY162 zXyaF6=k``1PvQ;j>6)sdvtV)>1)+`yDJdLfeUnN-mcbz=(Ib@NG~te(+3?Xh#Kb6hh3Zx9pE!kk zS4U8qmyuFbr@?dkY6U9G$K#aG^n?ZZO)?hSMa$U!0z{ zzwdC~C@n8Jw=F*#rZzbYDAN#*zO!i5?amN=>GhYkF?>!-SY zSWuuc^7S^8+J2yI9eG9+DApTNOrNjL!Z4vg){AIb3V1k-H*>bg(+*%u5Fj!N}sWJ+s0ZIgd3b{;UJL0 zxeLv-OTKFw?*Z|=O_vr~Y@u{hRpn==2=24lzuH@7x9hv?hiP_Zy^4cZms3?Z+A0vI z+AC!oO}N-6g-KMEB#t4{`18%T25dRV-BCoK2fyuO=wIXhE_SRb9-}kH^c-$c;zcJm zb@@Sf7L*4a$8^ML%j@eHqQ{VurkZI|oZkN10W_F_kA3BqkeAPML6xxmLzNw(N)^DF zk_ZHW0NQ+UUS=SoC?S%*;h}_v>E92SfIHCMM$%Sh%B9Jwl!p?skbthf1NI$f1Kyi6 z66O3vA}B`oQ2iZqRqiNNywgfdsDPK8N(mreMH}42;l#y4mRIlE8hUg2iD$h$w`_KQ zEZLmrX5Vma4JKVgYH5qBG}TE%DK8R$ND5Lyh4eje_}G|WD41rK1qzF$Bb+yw8zP+3 z<{1S0PP>&YRQdA1r{--KdY`vVk1#lt4ZNmw<%&F9d|mR3x@K+wi{CphIJ?z$|e$@LvRG{5WLe!PdcsmCDVOZHES8074nU zS0j=esBSP}OYIS|N>>T1C_;sTodg-YnLmD!j_2E0fQ61#N+-6cZyF3A0+M+N2vjHm z*CqF@vtkXS_JG*hMmtqju#>5WfmN@2&!(r9qY!1Hrv|cW~h=AN13CIDU2LnYM)+PuOObTj6~klzfYL;@}9JxYEGumY^>I=$-)JfvfQZFNl z0fDINhB!!gx(DNK7Ef+*cAukxIhcR|C{+;MIfd5lA&cL4v^GVsJ2ve=uS!%IaHy9; z2tAsO;X)56iPzTGWZ7{sCX@hNiBJY~eO#+^4r2?p`0TRHiVVR3D5kB`;yG~kncsF{ zz1_6TDuqky%bBQsEvL0o(BLW{5EO1m^AYQhOYlz(n`hi^pc?W8i(SPd#=nyI+?!11 zNlKMx)!APmts*j^vI%kCOfjrF9oLx4PPvy&bE?qCNnCjTG>y|g1jJV zTO?g7li+Re#}G9cj-od-fE+>LAV{5JKE}SdP{f2TC~hFng)yh!p14pL^mjpt9PDtR zM!`})Oll6KT6y)tg&PvCBLiNaJK;kb9uU3Qt|sa#NJkg;$(S<{uTOWs3Kj?CIQw^g zj|u9vxHG4eQwc+%eI`^7K7#<5B*%*Nc)h?O10oUS{Vy{TA2X;@wmOabgsPnI*z-gU zM_zX1JfdwNZWgV7TJqHA4YFcNJV8Od7+fH9WT*zQKmCdJk%qQG%)X@YDQhH8J9zaX z^Uu>5e-oAQ5s`9R=V0Vk%<=8t#POv#hEkNJ2A5%eHxVoLhc_%#An?O?fFMqq{C>$8 z$R>K~#P-pw+oTiW%o8xdl9cz)q$4V`W>&D_L=bvR+o05$gP^}$b}|75%hz9v(H%{;*6^9KW|nEs8zioyg$LQC!M7z zavIW=G#ze`aY;hL(v@ifbhtW!r%t*KV_Ye=vW3XW#gT_FDbhBWAI7G{!xHW;@I_45 zmaue`w%SsZ+5|~~&+24f%EaOz+lW7q1RAMa*>$xQVzX`j0)UYsgi?cr?lZ2daJiC+ zViS43kY+D1B)~VY5Fqu%h7X8mHe$!H^e%+PZx{Ga(=#2wL;nDjGt{5$_@raAbt__^ zDi0SQkA{}7;$@z+{{A;(9N3@@YSG2xb($c4D zKkRFVP9xa+KyIp__~_9%nGHiCAt36<=@Ec%Cr`Ucerze6os2%yR`Us;h*K z6+D7;15@>lZHaP%lCfY7JIX0>NOu*^!?nl`QL37u&M;;KpLVD3!x8`_0)_N27*VkF zq)$yfG@qY`t_v!OO+kA-1+NV?4+H55J)@{d3ILflfMVvu`RRa?1xh!UeP9q#vrQ>= zjGh@GWT^NFpNAoc_|q2x;DKv!x|in)EhpKlJE3KCt*nS-wZarclg61M^J`pUmuZmI z2zqjTL!U^x*pSaER0rS($T8A4v!W<-3M{!|I4ToNSEZo|GHqxe6F&=MKaV8FdJ(Rp zRi$n$nMa{`T$|F$pdts<0ddDPivgg@Xja;R;V_uxZ=to|{IG<82X*q{^BOb)!kT+S zJG?p;=w9P#gpoh1gcAbu0(Isu6V@;cNp%P*dYw3BT_M0$5Zbew7QkDDv;>F(!Y_H| z2-6*4%y&xB7f^#brKrY{GiSpi7D53oD#6J8UKKeOcSQdHb=ML=j+Q!4r#@igGvf#+ zn3N1$@MRwUO&-boU=t*gp;Nj%%~p`NX?c6m*|;=1MBK)|zg%STzMR0R0A^Y0e5>eV z)OInAhdiH5lH>8c6fM#3T0#?Q!%dzPgp@%VfoRvRl6)fg&uv2jS>9<}ySio1Us&|N z2*t+|%4eEj6c<}(l^)j!h9FM)>9YFfYQaD%Yqb&xF>?pdYI^C{0KzyU7$)kJ0T7M~ z_EKtxD8I*^Y2=)j0DfGZ^#i9Ah&8*PWQYBwHs-Hq$L46LknahrWrZt#-+_ivBvV!k&5U?%9wC-ePy!Wv@2a0@=bQ0| z-u~0)o6kEk_G?jb089cQ~<~J<33_CzAd%Q zy)sNtDFAyH_4$gA-QLh`PH6j^FH5y~$MhH07gp6}YS7D_;A4QO1LHigG)RsC(*SsS!o(im;|Y@XC^&bU^&mOYha^#HoXMMW!#H4Lg? zv=)T+iGnVf5qW`!!xGdG%n*yOoJE){q$+FQ!H6}rj1FR1F*w-n*#xTB$4+%{V<_Ig;^U$nKf)4BBCY?*mBVQPsa)nsN>y_q2U3k z_^Fl@q^No69Of;g{9^Nk5+OxRR@=$pLP3~|{rGj|(r}?b!%pBFN&x_#ho6=dC_Ypb8A z*@~Ca+o^V-hS&~54+v4=uveciEN6=NSetmuDWNo8^gV4Jr~G@z#?LIWeL$@%%|#bl z4hrp*b+fvUXq}YaZqLx>`3`c+=y{f%Qc&^~LX?}f=Lsf6ktQ#xzA(F9%eV;LBZz#v zy@qGi{4d3Rx()SE?)jtYx>;et%{Gae_FM(!tpE_A#0Ulm9d(d?vpA&1!QBL-yV&x& ztD~)QjeZ^^v&uf;Pa5~vtVV}+NY`yqt{g$7yUsw=13FH=T)JWal$(mBd->8POZbi% zuY4^?l=~gCG)`60F!2rsI~M>c900aLfsr=u>#e589M37;nZoXtR}FaPbVN|RQ@+ii zPqcZGDsz`q0C^OMRXQ6Gw$zZ42mtcsVb0w#M1G{IvlJEkwY%7@zGI0<1#77F46zM2 zy|^8p+p|}iP_SroC55d^Wg#jhDI|WGB+L)m=NwIr5RKAQRSj&JQsbDs>@BBp0@P1cY%uLO=tlI*nj-1m6+yaZLd_YMI{UJmb~8 zJ7%U(thwOcUQ;s%{{PLpGGT$@bE)QKHB8)CvhNJlEc3fUWhT{Sg~ zcZp#PBRQIJD@=r@Mvq-IhiiA61j0*o)s0KhJ@m?2Mxg*&BG&6`0x>HCV#E`IR<}{9 zzdHvak>*}2v>AaC%nrNC7L^~;w+S7VQ6ZH!8c%fu1Hu65I!B2ye_UT+U{5HNDR(+* zMN)})$QkCEayOkwAxd|yaP{Pw)jnFlNdO)XDc7x|uBX%tSr)5tXpgL zZHC@4O!ALtl!S#6c!1DeOw30jE5^dvJ(M661b8iXi4;wNuY+8K`Q~-F0;vYcV#mIQG$-w(D4{w7yeLi@F!W)s1*lb z(M8xfia+*{_KDB8iJQNdW%chTFGGq$?FUuDsF5}>LG$N)ddGoy797|@d87c3x$EYN zTA4?VZT|qt@i8%R2J!p1g-!TnVbRthspDGIByklLDN4B{>tY4X{Jr?nOM)BmD@!|i)i__)GnAj}47#SyE)+aZW#_ee;zY2h6`{O!xx&Pgck&brzG<&hlSA9J z+dsjvnPW8LgbHn73&9|C6C>R=z&4yYw`L+!L!(!a4-ocgzxVP$BpHjN6gj7_Sebst zn-=P}X;q3*kh>BLSZOiL@;vc|@ku60${I~>zQcmu$FhGF$1=_d8>|3RbfVh4UG$h) zuE}P3qsa{yh4r+vPk3|@R5FtVm7WU(@Pn)55+baPt!Y$2*H_Zw_*C9(|%> zT$FwpS%NM4gB#tGV8Ps#C6!SCM+nq0kP6|w$C?fm^6k3*e* zO=?1l0Q3euEuo*-+FYAl$PV%sLMVkwHe7w+()eV7N-k9FzM6_Smp8I#4`wPtKp*u) zK+yQew>?fQwq3S;;0)R}wWI4srwVPmXO>Yi%9^DJeE7WGJ8S8!zN{wWm1HPAxL3LS0#9bUx}9YDR!UOkA0aF zb;ae^9o$1otc$OwV zgl%{xS$Agt01n`VNp_qWh1Rl#Xpfuk-c^3oR8^t6g{`G2(Bk-skO|fa5f+VO-HoOU zo2HsjDJslSLhDX(;28L!2nUOm;)3Dd-Gnr#T9oXf4|vFntHg=1(2Y)~6Yzt9qA7cb ztR+b~!G$WER1!21rkX~*ejXT=xHCRq5YT^0wSthN%bvWxCr_3UkRh*^#5B}AJsuKU zMxn-`GGw1|=ZbBh{{XyZQ!htojwlx>^Jia6bqyU18Ru?x(j(udrw@rve#SbKDNMl~ zL5bbCwrk$3%iN#@p-&+;2mIER9~mUfcw)P1DgaFnA!04-Y|ewjaL3zGcTRTGt)r%| zWk)L*%4rc-{td7bt%z+r<4Ls7F1V+fRVo0XeEa_BBhb7qL%LGH)~4O@6jrmGnP~j0 zYBnq2B=RGNq-bMVJvm}7F-buh`%_rElkpt=OeD8~m!_(TWe5exBoWX9^XtpY6?n5L z52{j>r$1W3B>aazLlCcQ{{TOesmD^Kl*v&{m;+JhCh^K~GX`amLlpM8%TJLO+Z&Jq zQ5WBzrIMJT3WT8ITJo zr?u+vp`rGPa)kgoBxxkX={k|i?_7E2*|l!VF(ewJ^{;gWOna|@crl)73KF0Y5FKBi zo~j@YXYCNhau$Uq7No2s&w&8*pMTRC4Wnz`#RY%N6%Uusp{#mt2Z5xrtffFzOP8_Z zu0+Il+Kse(K2z;93f_H0Hi(`l41^{l{FFd3$n)o6u}*O@q?kI8?!LKII%N;Uus_1= zds?DyPgX`Erh&T#E;sPt-TgMrY*KT*pvY;Fo91Wf^TG&C;8|)XziZixVy=vifyM&YVyxW*krM8y%G1 zYb-fPt)SW(%Bp7GZEc3#T6h=4l9E9qLNw_zamwRU+;Gb#;RxacE=p>7(;-`GofB>Kecx@x-|ck)rhe;-?q#=6!qX1$oT@ zQ;VnxfJvK3Ctq9mn+r@s8m^hEq`GAw@g?0NqC%l%7C*`P*9y^C}Q>A!GSGr!#CJ3Tc(70%qP? z$DdwaI8h=Bp4pnJ754P5rPl61NdrSCL(Vm}6e#sMf`ANEu&ELxU+L@oVM2|mZA+Cl zKm-{IB7DC6;|dfI*R$CL5<$I>pO(MRxk7@Ps*r|~4x}V_kG(l^(+U&-a-+E+L>(X- z>GS8|2b5w&C?jx?NkW#=!3zsYxE2%)%RnpGrnr}WMQY%r#laVik)*w5Y`JdUMVepywTK|PH*K{KH!AQPYwJnixv z)KZ~&Xt3C_)fhO))v{Idb!ciSwyL4r&DE%$#cqgMl1zw>esL3u+2vAF#S@-zb3?)yed%tCFWc<$g_^XtK}$;6 zcA}-Uq&VY>9W|SRXUj;yCyHatyH*>JqwT z7N1M4@T{PvNhvZ92$bs+I)SbagR(fK?7?{0WbRWV7I>e@@Z~0L;X=#B>q~G@kfzC; zp+Hw^+5@RbBq}6Ef<0p17`xf9KN3XFyCDJgT$iI|JjAoWufdp9h%=uauc~);u z*d2gMVMf4-8r=K+aeXEZ6vS{B^5AXn7<^9=6Ui_o>R&Zr7QCC2ta9AV;-rE=3RTI_ zPN!d9Seb{1hbeZ|n}FomCm4Z)iYAr$9Y?>geC9AAvghQNNPQ7IP^uJs+V}XWwvOCIr}r?NTb)XjRXwdXz0j0u`)XBu`k8c;+?m!-|EXyc%tv z7C?TMfnvtdgdy( zAWCDbh$B9{EJC@tZi1q|$-`Lb)rUa((kij9XE(nVKC^P~yPi zBlR9btn&Bs5d#*RB&jsDw;|ZmD8=}<_YRp;5t(s&lMn~pgo%z4{i ze>u}pv(69@>^Q1ZFjN8Q9&xtqvYF>nZUt`26^4)MA}IS`X1T-u&l|#~tEgLyFa8q_ zAyx;2ZZ>6Uo>p2?ZEFl)ctOSg0BG#{!H?BlKip!a(y2(z_?9S}V#bwg^vECjvK-X; z!jnCBjV~}&B|!16#L1J(oX0r6Jv2x`QmUCE4AC0>W5eg3V`B8k)yd$TZK4w+%1w4) zwpN0B=Uy1|6X6FPOEHv!A%oSca(4Y|GNDLNb zD^e;DITCe^560*3Og1a9JzMq(^<^Y%|p~&Z!6{#z9O^bbqfi z%3~7nW=Ttc2uCKa)UTpkMYhBFO6d2ca_A`hq5*BL@9Qb6U2(LS<5Z?>HH&y(nAaIE z9GrdVL1HSX$@&94=^o?!G!OT4GZ|MzIe&DsLtk668H=Qj2m(X^Mxb~D*Xxfb@k~aT zDFw}4223uet*P|C@v|TTVvZYyF4+v3k{vZh@06GOks(N%=AZR(*&5X=5O|Zza_Yj~T%xRM9KqQgdASy3@w-Ge9Uw`S+v`~+@|UB$Fm&rn<{fI z%PERka3!=n>y3i0(b)!OM|;CVkACnCmRWG*D5zy7xrE>iQkA76;_u}9RM^vFtC_J6 z6e_8cAnVS)y`lZOws#}l^;U0}W>oGe>RPs2S|U~2G_-<}P3MR~7SMx_OTai|2z{u+ z*C2)VM{Y6YyJjZfoYly%trd_kx+Bv3aBr)5H9J|oEc0vKaZakW=a$)8xQGgMN*=d? z*Ih9$XE;F3yo%@<8(R%W%Hcdq6{Y}%LMzm_r<+82&$H~`IB{Xu)V8h?*q8|npU z=^WtTvrmSF1gcape%Msxv6hz{oX`>!qVrMi(2Gu91BnYtig>t+N1d(p=kPI1(vp7A zC>0HOhGV65SdlE{2rqWJXTzsD%I(W%wad;ZGMA`>^iM68Qm2_EDH4(TieMNz5YSm@7WJ)CnbqTEFtRV{Ntr|HA2Qw60w98C8}1cEq8 znY2N^BF!jLDw*pS`#i*i)8)wH!o6ufe%mXZY)ZC{lQm0qOj4qN8xiNo@eB2)VrJ^84s834v0@GeZN0`R3Nc?lz>v83qXZKW1<&KzBnL{KA2BNiHbJ zokWpnHrLGKxKZ-VOZP@yrH1gWkh zpYa;_Y3@HGg$f9Y%7+gTwdaUN?ENRtzaO5gh$>8ru-DFy+Q1Zi7?^1$v(~tWUz)C)ofp zD;k5V^3;)`*+#J7D1g?I{r@L+iku#l?{s|YDIvX^2S%jd@l^C-5Jpz z^vD*}5$K=Bf00in?6YESf<&n?xy&uw%tuW-nBR_3wOMTfsB7A&LeR><3*sC~NCmZt z1xgwN6QF^QIq@BkVX2nN!Su4_Uzhf82Jug;yS+M+5I9d>EL1p&=H+v3?%Or4Bqz5^ zVY9>xl0bx+f<&86zPO=X)mi{UHIqxFbjmf;OlOcx6wublNPq1)7>!wma+($v3QAmB z5>iMbTfmqzk>{RvSFGA zRp=2GiIC}I`n0g^I_PTKYNnOITkoMRAyeJ3Ac1QFHP;O$8jzrsBSgJu{#wOw+h&qu z%_AD)vXnnm-1JLjJ*RE88@ks>`X={$BJB)hVb`h)hWXKtG`M6IPIuvJaHK1S+|y;vDQ_(S4W##qk|Ty>i8ekq<)mU(CeJwv z0c?Vzj(oXQVX(HLhGw2}o1n}=FEFZD=S8@rL@n-boh8bq+pCr(WeVfEL@4#s$nwR8 z!?4eeM5Gl^5CW=$<4<_cc(07lF`E`_P&RN1VH1@KT#E+l6-T~p@YZLwZ)JIXO28^9 zar7wv0EvPDNx#g;u021%Ka9!*n3GGogiHZt2Ch;e-oc+X@TTAF=MM-sOg$1cr7m2q#aA%PUO)91e^1T6somYyLkdGbR~DAw;Z{Vkt*v5TCEuJ*3&%)llbIC3am_6rieV$|u-)NlePp z+lb>L+Qbf{TqPPcdFK@Pz5_IXku1rYWRAnXC>ubfQzRZ0Ax)49R0vRxpy+1eePaxx zF-+~B?kaKoKk12qC{Wa^1zewz;9r6I(C1Bp>W#2pHSqd-L56Mb+9 zfF+b+ObKvN8J!7K>S`zh@T+}bRLn>S(Lc%7+bw^TX6;}%gI#J<|Qz4pwiu3o|Hg(F>=>?#tvFxcTmh)Lj$CM4?-iacx) zHt``?s|Deh&XAHrqMm3u#4X-` zvF6omeG~O&g~G}ZRUxwmL6IOq=V7L!rXwE;3HYZXRVtPD%QY@9i(&~01R`dQrkUKS zir(8yjohlTrj5!}p~jY^ItoA{BmzJZk=H_c=^>lLWMrsSR((He1v3#rw0Q0)n1P6n zRPS;s{D3?t;iIg|nJy+k3RTQ_og>Go_w*6)^Fb>LAVtU@=^{As>`+wVgd&8aTQ5io zyqyUWfJcZ-e%*difKRcZ{7JPm^rh}@R8GMDLx@NGN>mBCX?{w<)&NvxZO0*~J$h@` z!^6`Y9h3k}(%!tdzI80L4-=*F+*BX$Q(fv^Sv-bFOg5WYlQ`o_EhS3VKxFv=A_tfA z9N+*;il{4Y3!aI}l1Gl>BjXx6%$gN+SG#>+7{BA1I7V8-ec2I&(7U3akI`PnnggSv@E@I z>lB!nlTM`8w|t@<`*dti^p9no=;kG`lTJ1!{3Jkoc)KFSq-c9yzNZ^648)dgvj7Bf zh(i_s0E0dI5m&WhOmHzJDu;HuPcC6?4F-2x**@tfnduz<t%0z_=U-b)^i%-*)hPVuIKjL(T zcWN^fny1o|4-qg=`#kyhd$}^HB}iZEODOHb4Mt+%aHy-VS)1NGz5MV$#JB|)%tC^e z-%CnL#kdrk&A^|hN%GSSR%JX_CJ>_3J&^Zz@|^v--?~4uSQCI zoq_gh8el$YUelq>Sxsb;ll z{{X$Dm4D{-6MJ}RY*x)8lU2GspSwi4KOx%39B}yhtbCcKZ{LV67D<<(RRbOw`y<^z_0EX>j4oP1u z!)H}$d|kRbIZCy6&;SC;2@qChA2y zNo%uVhE$QlP!H<304gtSJ^0rEMj5jQe6+}?J!8+jOKOlwGl~=C@oevSsjB*yA9ZfL zz9p$@DJ0wpGvT3+4F-gWSa^hD%r}i#QjT){@Q*v&_JK6SD-{ZD4-w@FzQUHSyh_lL7QmIFY@is_G<;y?XfJMP@Be5zEXcdBVqK zE0DldJBRgHOb}1g-KLze#L|O6hoZuPdB$EGTHZ=Xl6ebGkbHf5j0A-V-@GXME6e4h zDenu-uNV9HYk-hNVMmqJouhqwGh%IF&*jgU#|Bj(hH$IKyr7~~l&A#1V8Ie;xX@Iw z96VqFCNHnc%ioDmC`sn56x1h%0OCIb``BB@3KSQ(RNzomxBvnqiO^|l$okF{DAkoX zTdG=kjDIEvxZ#Zt2tuQbA6JzrvU%{NgYKW(_rM}VCG!O-ERRVaKfOM9P@tm{Ax5`< zPnG?^;1gD4s+e%Z9EgJT!kq`e1JkT~?SbJm;dLD8!Y9i(oEowdSNN_OoDeEyVwxg*t1TU%n+`YA#RL>0ZWK=|7`A_AUH;~q z8DB!pRcnTYPBc&olz3HW8pIMjqJ2E`#5O6VmSRE8>Cx&u?H;TAd&Yspl2S^70Dc@$ z{AgpP&7R&OwwrbvFHcD5T5H><%pD4z5}v>~W_96QP2f)};{~^1?#(g+&z;pZYL`Th zaPYmhTwR`03bXS?&rLEgfvwFd>fd#B1rDLPnLpc14G|{Z5zNjr_@wU;&Uv?fMkakh zLZcw7<<`-vn=!4ZOPz~gQzaZL<2~R9{UR^%@xVbVqIsI8VJ?offK7|{ge;*Hg{`gS z#bD1atl~8G9*zFKdV$cW%Mf@;G0SXf^ zdPGFqk7oyR$~@YuBvPvb0r%3L6W$04Ukq!07%|t%Z1xfYZ)Q<+_#RPTkAi@p3PE9Y z1%*Chpf~5WdqcA6%qi=rp3B%}#RB0%+h|0atw1E&W=~O#HWnDf0*vcp&E3ZjOt#%D z(#t%-T2fG2=xK1g4O%Lfd3)!p&8i-tp{P@ZYg$(7RA2$G3Frj=^ElFYCdhxpOEkc= z(wC)l>K5n;k1z3UwrR09PVoSdQkl?SK50U%au#Y8`G=RpwJV8gUwKlidGj;U=N`ec z&4Msx0L#23pg}}AbgPqM`8}&>o@Tn+T6||X@HzD1O*1SflDe4 zO=BbVA2EQB&ehWfhWAE1P)}F1H{KLF%EOO6o3n` zPD!cu$zWz#6n!^20;z1@*Qkl_?DP0~Z~NLiBYS8z7iQ{R%bN~xJ5td{ZV-~L&6Jo) zaZ$pMG$#0rN|n?ek$13oHqo^iCK8ef{{Trr(bB!VbcgsidopIbqNdxOnciN=y_Ic! z6>CXK^BQ8^N?U0vQ7!SN7J?J{l1MTj^2B|ls9q}LD_+@kc=b;J@Hy}dr*H~XJ$mZx z9UstTEexKR<$NezG7NByJH8rpgC0W_iT5oy8m4@-&z|ON52q}$kci4Sy*nPPwse|( zpnEmh{MDqq#dSSh4O41JC|gwwxXZFRl7*{Jh9sG{UWpt{v;9RV=E+)EvAGvG4FF-QeumSaK{0)*}SqMT|Jti3CgEd@Y=l>!K! zprje|9$4y7CORVVk1yI~5|W^VTPFgFD7R=QX1PUUi9{s8y}5z&)_pN1Tse>#QG(sC zm#jpVX$UnWE_Jhc(iAkRQF)c7M+=BlgRCf6#A-x!>81sWpg1bI)w92}0}m4ucrgPs zMCOk1+pqr4e$ninRk9mS#Ymf5ua>nJSp;~%Xj+iGWQ!PONvyO zZ-m$mUp`o?#My`>rD&tAK=!?3DU1x*hbL1~s=X+Bn5RAMdp2$U>#JL)&ho0Z>K<7P zwyIUn2p|FqK}y>(+%v^-J#iXo47CkUdRmbhd1kRHDX*FH=U8Lk+04?NwI%c^mqdbx z6ADQYVh98o2GJt}I6L5wp&Wb$eSOR+G0dNlAgHR=tIr_(mJ@DdmA48CRH&ILpT2t7 z;w~l=8t9?DDeSqy{{Y4cDFae?E2Gi%(eycyRIi0DvXzMDk$E2wd{4&|c=%*aDgyiD zs#R(69SyNdl1Wr`qZ+7H38Mv!nb`EWz9CN7JpuqeLe}N?Y&a-aD4(<98RnJBHFE3i zN|h-Czu;u4JVl_Wyl}u0PgoxR09%uZ7;`w9gdSX#zAqP8QcEWn>3}0W`+ADlR4|iw8 zIc|^t0LdyZpC2f#-K3JY)SHpT5$pS7)J7`OqVM{TABM*%&8lc?oGDV0lCv-kul!r= zVAA3efn3=Zyt(KgGXkWcMXE!SMYZpgCyBzVn-kzf-^Zs|$6`s1h~%}We4ta)pUe_g zW^1f{YB!ZDpChlw-PZ*AV9+BHuhze)6esHzZU-rwXk+X38e$$ZQlsZCu%MOIaW=6s zNW4Keus%X?(*6Zmfp&CL=o%D0o2Sp}@~WzD%Rb@?Tton+NYL6Q!Y>xZCebhjG9`H& zm(Dqjd1WCHN-dL2*yg2P3J&t})tyR3GX+Q^Nds2}bPUD2sg@wvG=-A3?PAS*1!DPYHF!qk%e- zN`M1j@-~>$7NfzXG6H1=iT2c|?&Nc}$7TLSB(h9q#g*_anhC_kO^&U5~784l5QsUI%7}Z{vC&g0Nr*j04c?q(c+)ZJa%oXXrJz7 zG$jC5xUd!BWR7}$vp0gJTOuRq0{X|=l)O+yB zZ&rw;tCx**)heZ`1RV&Idta1%`T60@q+6Y{g${;aP~Krn!bcLgb?9TKg}y_s1vHf* zvlOOKkhZi|jjSb9!`$_C1r-+?M@wD?3bJlPa zNr2RP);lyj3?^3Bd?Y{;ZKkKB_;Vw}2Y^CrKcp-z3*iv4Gy8o!CkhlL7PQKdX&_0S zQ-584y2cbJFOtK`S~;pY`RRXzd0~wY0}d)Uh*ZyP$U2gQ+n=m@;3#rEAn=vW(6pp* z&auoJMBAOb&KV9y6cZ#6ksytA5zi7KMlODzBg@F(1QjJ2OY?;Ql-i{#f%ED-Z(@35 zfD;g05?3vBBc+#Fdo-(t`BOUEPIX8!pjz5cKc*KUT+C>GdTKne#@B{1B?T2(Mrtdk zY#~b_l44#^rl+Se-2_ZM$>v8t&bD_dLW-VyG|TA}k^0uBYMN*Ok=`@PC(9X67s3)@ zpSFQiuO$A+sz#^4rkYG?3ahJ~b89zSB6cwT&E*>xu^R!G5GZQd*;+&?q!5(4;^`9O zif~#KOpC?sbB`qPIfvAmT-1J*$oOdb2f(qHW&q4V-MP^RDkA&48vbNgP^Gp?*f#6} zyUCNUOMe*6;$egG*_KO$?$LZPK&c9qQGFScSA)7d#G$nzYauI0ErB4Qc!7BIpQke6 zUc7i^^QN#F=PC$Cb+Z~|nfcZ|US!LE_mZ_G=7>sGnE`WY;qx9{P75TZRSPvQPW9m( zPdV#rP-~YTI2xq>W{lQWICxw=&b%fx>8bm^CCb2Bi~DbAi8-L>-<%8C_p1|B(-f|g zP7J|RKo=bTgVNZWBB9FN_v-`%F{t&2yD87e_cm6)E2_wHw%w^@%G#xGwc|=pdO~hidU@R9{{Uq111{;RAtJQDr4y7< zZJRKhPAxL2w-Iik2T0~$+N1El+f9gh?Wfr~B8;U~H65KY89ZDeN+J*w#6gGy$k=0h z;eIKFJb~quiYWk;%Ib1tvd5hGw}GtW%PBP^fQr6LksPQ*RepJ}nbyfxzeltgU0n;* z>n*8EWoSVlz$!|>DY)h!U&k8_k7~rlKat%5T@@v`UXOrIDuY{+!~TX?I85#b{f;#BFJ{x}=1O1dHlB z$F76{Zl6M8&PXOyEVnr)F52++iH_;rHe+DU{(QMZ{{Xm6&e!jId3!gplxpC2Jk2Sx zu4QOCyg=83|CFP{$*_Gp9r$>9fa*TvYF6@1?a26C=OMzj{QB8y`M7A zW^KuA#>VAUimk1nqD^IXW~TU@an&#i%GJy#RVL=f!vUIfC=qq^5$*m9vEgj^a>{VS zpacx^uOCAagDlJFb6mInCCh&;{{Rremsed(*lBeX$g&cm04J1>nZ#^jt3aT(sNGke zMBD7UG_rt_Awh1ZU39cL+YOzm#=}coC1^VA$AdSRN_C zQ>`@T7;O&0)jybjSIX9vDa8_|(Wxuwk$E%79C3{?dY51{De-P*kSS#V{*?#5xxq6z z66rXb%z;AStwCMV3D;O4gKa(LF;9z)IGEIh7O-=mOXqmBz{NzA%Rx{m`RX_bN7v-p zw$W8PmSvPpy-vs~OOBXIqHZQZAHOU~!`i|m#Ry8|)M{vARyG#=h@g%r2?!qX_vBF%-O09q^5ZbNTRD9qg3wbm9`;6i6nxUgF416Ft&~0({CxUQLWTd%T$bK zig;($`h#?&(##hn2)z0GjzoVIk7u3Vdq3}gEuq+2SEwj9ijJnXs*I@iEI5||X$+-W z0FwY(+IU#(>%10<-Cvg$=N>Eki}?Ql_;EDS1uw;Q1G>dcaUlxgQb_@1i)ul(k!j_B zD@HDGq#L17GdeCs+?KuAEeDz6U}Iq7kVME1pb!#@2>C4vY`Zz3ZEbeQfC^-2CPuvO ze#Rr?VIUzFCiC=4zX(1aC=*M(6Ez4)u|3sXM@7Gbu5Gq?Zq#O(=Fe8R)=y1MO;?#y zwpL}fmI(w2(1KvagxeIj@)0(qhOO(P_MszBuww%#8lzcR?RC|fF8=`9gWEe{T;2V4 z&h0XuL&;(O(Q0Vip(`Ls5J`v^zbtd~j=%gT<}V~f2y3tHzo>Nml|8YxOETSNPIelW zq4%J>l;?ed8d`)de$Ossq8s#Ba}I zcBek7$tj*_RMM?2yV@-wAf%8uN{9q<1V@34RpRXOy9|wi&#Eei&p59`y*aIUuNTUm zkRRF`bGtpg4V!+c6!gwl(NMhYGwBHJ8+iy)k-~Zvfdm`QDR!(OB7_6bRYi*b09hCq zluX2;xCh#Y-Q$`K>29-X=(guEp>mPdnPtaA(uF681rypPKng16MA-OV427I4Ao=A= zyToi<5iukMdp?%7UT%gPhj)LQB%TyFL`)Gnd4gk>w~TR#B&1Lbm{-+r$_MmiP@wQc z{_ovOU)apmqCd7lv4ESv@bNaE_lj+!4M`zOd}xDBLS+vw47KB0UgI9ad+~6l$vd-?(~Eo?zetm&rriB8 zd*A?%6xo$W{AN%80A|MT*3cglAndrPL2N(%teyJWDl0y9NH6hpSSQr#f7Y6edceXm z(>FIBT5G~QVj8s6HY#0!B$FqWq#u`Gh7Jl4a)vAk$QV*lB`atvnJ2<0(~!Tvm9Zes zZd1x4&q2r7#^&20)ftiF9|7m(d^Y<30GM?N(vT<(S#%TDe*QRvGvbBs@r4Q2;JE1j z0LY+y=L;F7(0qtc^6t%|YtRUY0w(@iSm-C=jLzFJ0&If$1zt9^Uif6FWe-p~mp=^_ z8@qJ|m}r!P0U#1ACOkQOkES!YlaMA$T#_E^r+hsFV+|@Y0l>XEFQiEvyEJjI-vKev zDkDpRKX@M#ijSyN(JE`LZO@BE)&W8?UKXPC?u*R3XewI?;c>*NH~nYN>1}4%Z>XUZ zKDW{87h1*E5CsA&X;%03d&85K?S5!mN_$n=1N@Im+H^fXj5hShic^dm>FyO(^zXYu zf}3fxHK@yqAlrl$NSM~(=p#tB7?rEp;F9BgGbjoRI6Rixn&$AO%5z8RD_Ch^PAsi5 z(t*}N0LYL?fqza|79JP-(1R;*0H9W?{?as_8{&fn1k)-xdTK14OT_;6U)>wqb)Biq z=9yHiH&TI4=L&+dne6G5mNLN={r@uOu4N zPH{8+rz?i#cUMuC(JGg;T{6(aq=5RIT1al<Y#0VIi43IDD&?h z@rkCAV4yV5Uo(F19EW>IpKo7oI9hjsmg7t|&+*WZY?~vFDB6#U|{5 zLKGHDYPr-EET*aBJ|nkcV$V2IK~+#{uGT?fo>5%8b6D!@o>3|)TZ%}7s&DZ53`{s+ zW;pgDhH_H3UM2qkxNO45eQF|8H|OAeE_q%s8Su~&mNcg}fWjj@bSSKXMzi7%-!7nY z#2LRM$G((_5L1>p-cGQ2fph8d5v*gWKPywT*V$o0@q(4Mg%QM3kQ2y_JiI=gF~mrL zbn1ensY(b@1wu>{gSdxq0zDblENG1f3tlobRzv4@Y<4j4`{{VN?pN0W6 zmmNFrDCP_pP;s!84J1hGSasc*w71lk59fw3KSJnx)kD);XFX-U_l)|dJ=Wsq(d?dYy2hUis6$ai*zlI*3q zFCcWczu$!ldX66E3Mw08hx=(0CP&^xv#QX^W zt03vu)yZ{gdBC+mND1fANs%*dUtMDgJH?O^tca;lKz`~XV0#PX8HUq)M()=kDD9I~ zwK-FcB_NT))9HCKH2(k&e8-5KuZ~YI)0i!($hdd@mM{Dd44hm#iY2b7r4axqEYaBx z{Zy3K(b0>3Q!1jRLUj$ih7t%PQN%~Dk3SjlId`$9m=GR^UJ>rT3^y=kTg&a1P9ISb zY_m{mdqg~z*3j=DM|v-Kd8?mG7pH>=ckq! z%%GqvX0D<-63jz%VxmF!o(Ay-OJPV z!Fj-E@a#NE3j(6ybNN<&aSOM<_-bx$>%X$5MI zhZ@b3#c?)yM5GW=9bT_qwfM~K-^XRe-aDc6=|>1GsGQfAziWTP%Xs^M+MLn4cF~w* z6)9Vpdu$+f$Y+R*h>HP$Ckg=;u8x zMiht#Pm@;V4=S3yg4)_qZEZRVRFatl2m)i{uZ}f)Cy2{5fD+^f?2Q)a(T&?SQG}rp z0wOO2D05V`X%Z*duYAAu4ZW?F-ZJv$vnFyn)KpYcIylr=2fkXFhXN;nkMaT_;zl&) zU{RSk@zW)KXqUgo4jvvPyT<92>QALAna6kj#a`C?2klq75uN0QpDS3WO)6+A76oHY zsje4P=urwtHxgi6-Z4==B+||lkO6jDTbr?9x<|M8P{70(MP*5MZaEK*mx&2mn>Vss zDyC`Ujc&ftEW)^(;)H-xBJv;(T3Y2}Z5Tq~!_h&^*2b$Fd_SON1Q4_!IU1BSqPCH@ zVs_e!(vd&$eOVOEQqQ z%#mhh}jHLTjB{{Rqe0c$X*e6qBcUQi7w&;U|I!6@S5 z8q7v5yfYkqrej+!MuA(2^d>c1Ce1G0hrKEgWhEGtM>!!B4eb!a@d*4ic2C41n`=Fmx0#vl-)fy|s-lm+-CIe3`ZNV&^rrliY4FAIn3*Le$;B4d ze?uM<{=TkO=K*jkk0 zPbCzY8o&q0Mgu+xB8asac5M8jH~5N7`@uUackGCsSds9tMnC-a_*XDnjbhPtr zI%tcagqH#0Fqzc-W-R+DLe6-OVfbWUIA7vIenAkbpMP61qQ&g)W81HEyJYna&C%^j zo_y0&Q`CNDpj0LI$VvhvEkKnhfS?YB;9C`&R>L&Xw4{_Sk4&D)0~pfGK@%y_8Oybo z^$))Nw-=M`_ROPevZ^Y2Y{W7uJ9b+M53rz;X4fQZ1HfF~ixXv@S%@-`z%?sMQ5{{& ztWLt2L%O1;nv`!r>jSDcS4Tt(F42PM2mCp~w^^Mn=YUVMlv7miM!i!!Ub~#Eo36XA{Y9f4}mwohtip|+jc543sx~xeP2_^({fzaMyTG%)E zIS4uiEWX#u5kGOQc%ELvlvEevHM{z3m+ zal+jXLlI{o7>d~OgBDxjO8B6tf>S3!^O!UKju!TlRVnAf2L+XKQ^Ox(55(KU1DaAX z0Yj^B>cL?_D@`kjWF%Z3pY=o^Gw;J=1(hAgy|w*9hvRm%hx2W&1%i6Ux=eMn9dVl5 zq$NPOCG4RJtC}5s^nMp9-;jrJUrJp1GB&f{VIj5=#bLsdE;TBAR$>9-t6tAYeW{-aX`&O!9_+Z_A$@ zI}gGoO*t)0?!Hj5%c_)Cg$ELw0#*-AMY{Cy^23S2As&e*{{R3`3v_g_%KVlu z?_oAiFWAkM%k!!>kcV=tsg~=?0(h3PkNKpIW-dv!&moNjvP^K~`%Oi)dk|~yNRDyw z9aI4C!_5~crXO#AZM~DFsz%Ia%vIIBMOyk)qL(`m5hy}}$l)WLgRJ;tOW~LyK_ZTX zR-DRm)PP?dPR9z+Gocjef;C~_%XZ?jK&PYAIhe6{emuZB5^5GAq* zSOiG`2shUb3J^^J1i}>-*C;H^Tx|`gIA??_T@S4ITKZ$D$Rr^O`F7s%Ax3c4W(T!+ zmV`#V4?jW9MkAlivk%kK4#a4lPywDdijuHI5--oK{Cf59fF*(e0YY^qB0!Br%=Fg&G1mc{0-z>`O4Z8$092s{Ae4Zs<^^@v(3oE3_5G^_JVdLc z$^QU-Bzv#|5ZLg8iGjH0V8`>0)1a9k^Durk&hrV>EZNw=h7LnzF35TC1G2v~rvJbm9xC`vOObEjBH zt)d-UiBI{H?&sy>?zR+kDmaJ{`Gq8Eefpnv5{%+j=>Gsv@0@&JF2Eq92)NYi;ADEu z@RVn#JfUun^^Y%@+?E?DK zDvS46d@=zfkQ`S<;Q-XamBdnok$4erc>+h%0EuG~Sy1l;I@hPXW7$CoS9aIa-}m8$ znumlMNyOI<;VwT-7VG#k?&*7V?QOG_H8kbQ9FY#@rNWNtS45yd;5) z6Okm$X?`3z>y&JNkKqG}D!Y#=a=GYiW2KL31{aWR{&dv;0Ai@L^%q2?4TOMvjl4X0 zd0x~vruaV1Eua6-vF#Sy3g z=nVe=IINotiN6!0cTHl-LQoM@bn{p!R&z#%+)+%_B#=NLg(}cVv=L*2VaP>63iL$D zRCY`>$_2quQCA!}z* zG)HVK0RV!ioo#y?V6r@AmS!q?{NKFbeosc_b$c|lt=|==otC4;I30lRueiGZ~+RcaC{KI5vkx^Z< z8Ij|taUsPbjHpKpq!fY3Ne8ck<84w-CPEamBJt5dv#Unm!8}5FM6#Sf03bO4u@$KA zB9Yfd%Vrr1`2`(zaM^N=OQm(7{FIWU1x%BBi3H;wlWLm~ z$^^n?K&q~8>rZ$j+a1}pyzK3|?TgwK=JXT0z&LoJN*> zRS0G&Za(BBUCDzsAOy_FvKbwW!oQT#4^l^LIp>E3!hulIt+cA6-aWqDP1Ynz?WF$&ue#GwA+%~jNy#W zFIHQdP}HsV-F1hHE*95r2UdWE-6H8a4RM~^WIM_wBr7G8%#;BuUIH?^bhx`NDBTk= z6dKXJ+eVAB_8r?i!+3kQtITui+IHQfNJ@JcO4L~$&_wYGB1H0!3`p3Cl2t4~N9e-w z=%2*=LlSH$MElfBxjj)pMj67#eY?}!J=991_rv{;X2|p$>q9s)6qOp~u-g$r%fj)rJ+q zq^(I&1d$-YJdeov97e>EW&{QVD^`idgsc!vxhWEmq!$Eht<^6U{{ZY&zbv~i+M0?? z=V}_tHg7VcQ&m(oN-Cv!p210RrKLs`qa-AranBtN{{U$HBEJ~o1>KnO1=Qy3{{Yn? zjP}Iaz29bgO`2u<5m))TR-7wAX^_(?aM+YBBfJK`WZ|LkaWMe=k1|nRZ_2OIBJM%F zyvmCxIY6j!2<*V)3L}&rSams=Ki9?o08|3YHuEr|+Dcr>*VNlI>IgLjWRjTxg>@0A z)XD31C~6kEpFZ=3IM{}#*8B`fU5WOi%=dA$l)1jkX3PBDGpyA(lTf5nsD~1@A*7NA z8IBZJ zjP{0^sy-JT)Li-E>Z-+N>v~OyD^rn1`v}oJ;5W~ISoEn7%LPR}@1{BQpN>m^<&dPgVwM5+AE=Klb38rm_wj~0^=o1s>%JmHV5{{Y1i)kJ(S zp{K+;E>^y^g-%vDTS)3Uct^-jmIuB|C0O%l1RQD8PF+5pIDNsHg#}e`ct@Y##|m%} zQP#YS8c)P?^)acGp(!VXSc`SM+X5~OIj1>&L_J#o`de{4AW4Wm@=d*Md-+B)o+m1A zBYmrDsl-q5dA|W6^o5kbPPLt(~hCHu1R88vYZ|-i+ zk?0#7-|pp6MhA$<482wy%3Y&cd#Q3DXsdzFC&c~V74!ZkEwDio1*tsfdLGOl(rr5D zi&cYe6CF7~o>s(J>O1eJ>gx!N+PX0*u6bEcM}@XsweGlp6i7;K0)F)#ns{Oq;K~cQ zBA)t3QqTUQ&)K0(Nw&R=H^*W?{X+dlz9-)uFw$d9DbW!J?R(%LppfD$TiBy$&f4z_ zD%sT)aMQTb(*U06DWB2^h_sGUF>B%A;^K+X5UQ5p)ljk&P-BUv01;JD!(UFZeAOAY zU9we`88&i-uPUsiX)ZRfuFr#qg;1E&gpmMGEl9@VPz;iA4a_fnEfKK@+?Z%tUMR!y zS=oKU)9iQkyp61}IlHaA;)-73@9EbfS@48FE%r}L9hngTKZLZJYl?GVGv0! z<4cOYDP**AX{2}_lZzyiepn26MA@RDtd+&S1GOATt;B|RezjfaG0naYB9 zdrw+c3bRV$;v66eg9c--QhYpy2^G_RN51<;gfV2iRJC12T~9v0XWfRI@tc9MK0)Af zTmGTz37q*#X&f&*uJ@Z`oY6$F+61HI!*a&Z@I*l3(AE; z5Rif>#B=XhEdHNRpb)BM@eN5&FLz#lw21LG2h-$>STfR(M<5`_!|%%q6ngGZ5f>ub z8SAgv`bvcX{{SSEpVpgo1NZ(gp+Su>rUQhmE5$Yw?jL^@3Ir=HVx+)Df8o)d9ANo^9qS5TSYZle*)tFM{+iye}T2`lNLKCIe1_#sWIP+Xo zGm+g%k6+nRTo4IQwDoM8y2Jo)Xz1HR=QAOP5*5NhxfjrV>&sjVaDS!4UjG2UZ6mxr zqFGkN6mwDLyzcjeVcNPt>e=t?9O9NvXp#oue2nY2_r%v zbJLXjjj`4kAvmvn7CIA!cGkYpy&GE7wKY{VZ7C{nNseUr@{WA{*s%c^cd3phpLo|w zzdk~cxZQqaXs|+66LI!jzBXp9~UW zMjGfkQ_uBOiadOOtBL@1&bR$_a&0Yh4Xerarz;2@lkC3PB?)jl{kKv~YIK{9UKp#B z8exb~nw3?2{Vhh}6Qq_=ASeR@PqEU(MS1U&bXx_O(d{Kd8lkGGc9HHDooS`K!>5RM zc#|POkq|ZbT4?oys!p#B4Q2b&?cP=J|y{ zTaT?MZ9xITQi1>kr0E*pkj9G(?vxNgD7uDn^d{_Rm8MH)+r}L2GlHecgz0@3lv~HK zF8;TNy$iPE?!Y`(+SYN_Ae4fI2vSLi25wA^J@=d#Z4$6tp6H5h zs+Z0x<--Pfk;ALz6+Qb%kABi8bUWbKO5DDuD5p7x<)2eIjYCZ)t+$Yc6sfmB?PG`t z1wc!+a zqMSn%?t+#CthQ7_d#P6q#OZBuz`?^ILK1})2cb(Um5a{{@T0MS2q`KOfd-Y~nl`U) zIY!rP?JX5nbF!_qm^OwgDpJg$#jLd2+z3D`@Wf8n@ z7)vDJkf>B;SN(D09GZSFp4}UJy?pUoqRh4#{gukHs<$0(rn;3$U2ADk;dPe+LK33@ za3GYBZwDK!35U{^@*PP%dL;~ec6~d?0_Lg&aYaXS4${ zDkO&#uJJNdzGe!E{{VitP@yiY#pYIE3(pUS>^vjsg$e~RPg|J@4%t`XNrd%2fA5|g zDuk-H7*GM@PfbwT9$yleakas;sRSqwDIRyU^Cldtk1fKL^%-gxSn5}wMJI*cv2{O& z54GJU>Gu08$e*D>PGOkQAiN)O1w3z|GZO|lrVhTiFN#PpaOB+%E*9M@Yen|RWs+1T zh()Y(Z|OrvZnbZ>wc9b5=eck$Hsuv9VOjw&D;i6rssW1_y|ojJ-X;kOQAPf0qlaeS z4ryk?Kr%V?(z&DYgeqzH2O5HuKmf_+NfYVoc(mfX7YRY?yEKmDkN6?WC-MndYkhcA)Bc0RT6a$81Nh>eDH}tPC;R(7aZRGuR{v#yhCk{i9Zq35#%aD zi=hXMaRZ9%JW^zzbo>q_ODG_cP!HCSpsuZH3Q4w|Vtx8Y#{!<52wf{!G{1=NziLLd z-e|WIejsMrP5kXYX2F?(vmcLG=pi8;(!bn0+Y8yHZ4jbKRB|ysJ#XXdjK7HD{%p!c zczG{jBURyee{`$^xT+yqu~k#lah7{Dlh`&wN{0x%5;<$-=N&Z0M;j4Lz+FKc{{W=Z zDn|QclkAkbB}3Vqg|M*lg_2otjxW*-z|fnBkBz@;L!5+TeC+=5YlaC8D7EfIy2ZXfwj$yX+5%U;5n&->GVkUbItox*PYY_5B$9pROmqX*aSmW+QBqNO zEqQW|;Z>FFo7u9IDB5x7#nKRxt`#oHkp?b7zpgu%XC#WHPm#+fzepgQBmV$>_G144 zeTDZ^vpcKU8cn><=upPiJi-%CRZ!4MxSa_IDFgyckU-~dxY&3$$sofMAYEC&$Pen8 zz?3_REqiXUDEFPceCuVqJpTYc$Wfo=il$qMTS);ypn{Xfo%GAaN}Qf>uZbf<(={EKkc8(iwpR zbdX4~r5NQId9c*XP9&g>R97VOT__o{t8GCm1w{V-Q`eLO>xq$HKjky4qPMqsyc05@ zl&q_)di{>L=6}X+7o&9VIzZ+MnoJBMGqiT5IKR(AbrbTj_{J(tO4jiz041)-HUdX4 z2))lghPvTH8Xg=V^@j>Vd%V5h4u4-Gj=q+X^!-2})}7F^)66dcVcbsb|QwgQqwk|H$ac<>YBZyY)$LG*#mi7Aw%!C=8n_fqc(DHl``Ao+Z) zeNGsKRUB7Xfxk9Qz`Lhy$}}3s2YsSjDCd4xKYGpjU^7aDu}tCRn3<&r>%qr=)Nf}$ zqXLqD(a-DZaG^$|wNCBH)R25fK6=Mrez;Jh zaAB6eN=fM=)64JSrWr}^Fk752Z zhVr0X>0$1HVPW`#s=A+V@@ERxie(@;hlnAgC?pOt0!b!$$&Va){O^g*<5i~u@#zL_ z%n@Rq@8uQDs`{FmhfP5ar3n+nNih;;bn~~n2HcxYu~CbRWhfjAnukZ{TeWEcs;Y8j z(>R1Eu>6UF2QdWmkEQUpChXU}U{Y-ammpm=rBE$R1<`X@?1Swzmfv;KhT0McC%gs8 zC#(y7z$cXr4HUP0{{V3+Oj9)q^H7a`d~Xh0+2{2Jim4u!f(li{V^7<|Plh_flF#}*iJlBp>q9FC)q?#$9m zNm9TNov8MK%G!sim|~@;*G%KEQV^~gHj7A@@b}`+4*<8?Mc#q@`TNA7i7bkw3KFFf zz3C2a%kQ^%TQm~9t2B4BN+rH06%a;{Q~?90r$^ z+g<)|mpjWJ6uU@i+$dNYB}rEa;UOtA0P_Hia7;pXtzlLMMUZ+Q?D{GDWxQW3y;}~IgHaxtcnS$ z{+0aUJ+_%XO`T<5m?&vEg8>R{ZKMv=i-;rittRSJiN3mGRI`b&e(8WHE`=MPuSjtC zl9^PBgJ9wEOtNCtjT?rAmNP3b-cXdfM}d*jqmIq?qMIe6qMXCy}@VTp&`EBrG8|r?V9|pZZ3wYfmu?%C2%P#97QSuVnBclN%-3A z*b>M>sHSbtT>O@vcjLQP)CCUkQ$xfz60V^*Rfx`W+RdZPX>!1)RYIp}TNLO?2@6xm zNih+ry}U7hg*2j6P#9beBHEm)c;SrmgE1Szr3fi>>DP!Z^KF*?vrWcjyS>V?TC&wn zU$itCmTHC{QXMoj)ku7{;#w&^6EMqQhK#%sw%5)>(F*2-%frc-NBceCOBYSv(LI1JNNn+4PL9>L)F z*n@OBDBz#G(HYh%3Xgl6#@ed7{Nrjf9Nw0>^{Gf`F>kZn7EX<<8E-J zOGf|Ok_B75R#xXYR+{le3jy}x6k;7Z=c!A zI;hD10H$^fCH~rwI2M#DnaZSxTS|-r#MclpGd>uVE++0Yq$yIY;He3!XOOIUPx6hr zTs`11B`q^8kvNo3HZfoR*&f*0AGID?lQ?!hn=INoMha>(R?q{kad(O0firtvefZaG zc$3d8qymkbXj-bR%w9f!{z0~VY3D^d#DQC?t*UAcYRwhhOp=%inNKsOyovDf>(>`3 zNlV5j=}oWy011UPG_4SN4skaY0N?%c!b6ehU}jl`L2V$x%6SRlAOJdu@%MZMh!|*O zCV-gRPLj%kj4CaCIJz79j|?bCMM6ilyj2QM3V2jTx_Enrw)kN}gRG}5%`MrL1VFk; z1Iwg(MieO1ndM7L)~65@xzvdu{R~DWV1`oXir3&bSS}T&OOB22=Mq=h2VypZY`3+N zQSAGnimH10M*`GJKqzn&nIsDl*UzpEFy9jrLk3h{fO|b6Y`DTgiWfv>&bR3wp=YuN zU$!)xoogrCD&}(>+#!^Q8Fem%(KSqjGKFxJaK)q>dV+C7v=mT6AvI)LWR}s=F7^^; zlq)DD5$J(ciLRG5g>`K!%{=QRFi_%>NaYJZb3Fzsu@B~j6>b`@(;~Fzak7F51RE4e zit$GD))LPV#{RiGT*;nc({KO!onK_t$w{o`MQ%Q*a;yH2=27bq`F+L|h zHzr4P^p!VQL_OGuwch|}MUh9v7e#1aNv zN#xYbN1$wiQzWb$FP2uP1IiuhO_f9KAzTQXa`5_G`>%l-9uZ?8Dy*H;yy0g}vgm4^ zqz2Mv2`Ld8YiRp7z#>$E0_O_)Etdry)`Te~CIA2eB6(Zuj z-PL|=x0!ZdPJ(HxD``;a;86j#xJrtQM+p)nkUcSFCK85Y-F2=0A*Lt^j)N$&jgn!# zn%P>eAxlR?PVHNu1U7}TkPqoP0Frz;a>l2^aMnOL`7k#?miLHzxawU*|T@4gjPOv!95Dp4R501C)}NdU&CF(U>=#|)^v9r!&p zzL*~t!y;Lk)Yq$V)jCC4)#jD2r?Xq`Iwe6-U}i<+$DqcdSrZ5-Ant$%C-+JYQIVQc zyj-KQ_6pT$?w@dRC0s|n)cVh_L4ZJDooGrDL4(9pesE7ep!st@;Wzy=dKggO=QRmK z$w(uLeGH$w`>|CuPZR4BBXufE=t`6U34j4Qef`+s3XUVz7?cF$BYKpk)JZnw{23Y! zn(L;rro&ba&r0d*?-1t1N-(=PuNGv3IyK0hL{G1uKd+6jAEpBpThrn?lMpdi>Z|Ff z>5$h}kqR2b+pLe4zYH|EM5=jvlYfqf7>a4quTHPnbe%$YRBhK@apC4qzsn6a6Ywjl zt(t@<>I*GL7%z692^w2LrO#ViuRCACNnT$O-Y?>Nv5%UlB$V|B_~w3oT=0+Q&R=MO zH{~xKbLrm*J!Ag>4zD4O=loAEQ4(bQM?X^l9Y85q>u;DI>FI!?u7lhUcrG920xFd6 z(h`;Q9hp<4o)SzAKK0ku`iR@IdM+I z#Qy-@5|FVWKn;HV!f>|;3k!B(Z!z-;x2W7fGLgeJyhl&J=K`LCkFkzp7XJXYZ+rW+ z_lsaqphs+ph#r1=@|-A8ZAhk75p6nh=W(UA{z*`v*)0XY2FKzwff0X~h66Oaw5Pkr zSU6;m$nILXE=<1ngPIy5w;Jrwr5+^kgC1l6M6anzuQ)jTMRsZ2=XJSe&}EdA^+ziyb1`+ag6%MDz!R!O zh`dZ&7_%@Kq|_Oc%tq^IKkoAW!#DNHD0%GWw5Flt7*fy@Z}-c?_?Q7S;}jUcnq)b- zjdZ2zquM-7x|G@pHX{9g9HSix6je=tcgwm&WGF}q2Wbw49ayHy!$=b77f!L}Cf4Mj zI%|9mYz$ILDX=Ngso5edwjqg$hq^X-i9%FLTrs6zN}S=<-QLA*X44vXDQH|?UgOQB z=TO@FG&HrOES02i*&JGt5%22{0~J6-Gq|PSEgNP;o;7o{A?we`Df^`ur)43Wt zB9h}joM}x2gapW7L;^%eg9OZ6b;ah*fIu6@woII!CopRln=giaSyZ4XM}x`I!U4HH;&G5fJp%c=^S-1mTbznM!t3^<{yy&DGJm?Mv(ylH#~f zwIm29x+O#l$-S+v1^yJ8i_sACK>nI#Pm6&x)C@|dK=bG6oCnV+vz)zz{$^KCQaf;@ z5LhET&8q`!AR{$nKZAS8oBqLy1Kl0sLQjrpEj8_ANjx7tbVf}og=@skIqxL83r zCeJVk9ZM#4nVN5ilSrq7{SfBDh0tY@ocv}}Z zbUc5)v54Ac`lQ)MMKtNg(5T&R&gDq_-qvS`Lpgp|PNku@v|I7S+OGtjFb`c!UNAvX z#Af{>JeU%hOtnyjuWMSBB^e$W-Q#YvJKKu7jlFG`vn;XN#3}E~psl)kR67F0iaW9( z>4V14_|Jr?m0U8oCae=9ej#Gyy_fb|B(2>(Ly_gw6e?)8(Q%hrNnE5SjYKGG8u;6$ z7|`(Uk?wvC;6&kxgtQB&Y}8Ou9Mw?7nxouq$v;z4^+gs~w6d-~6}FI2pn_rn7X~BU zi1P&r(k-z(Fv&nd0tG_bfC#S?$L+Q3(Yze5Y-%>|WNG_-mk$F_)e~h!rJ@uU2nIMv zkYiD9SOEyeAWgH)B*8(d42gg48$E~L#?tJ5%nY9~sjGSBOMDGfg@q@EAf7RBB2{e! z9Ca+K!_F^uN#>ABug@U8=j$Q%2ik8rt5XeDJXvMJgsCBf_uE>4P(Y1Tl0rz-#CV=i zq*cl`zom&-_?yy9rMFFZ8wGDtIm8G2NZQM`gDjxhysD2it*TG<`v=%}vriI-coLuS znXxvwiN%i1gm=M0TqBZd^;&w;@#8}AzEbF$U+>}CyisH~%>sHmiADT2^i(1kbO zN`X>}pd4Cn%VNFikz9N)6$;^z{{WRaEs#ENr1nGhVBSZy#`rdObi2LEE3+E>*0qFJ zKTOJsC@2f9rmCg&@RT+iY^^}JQVP?+`*ut zgg|1B47bn8R%pNrBvDkcy)6koysk6-5;%ZQp%l=GrPeG~qXT)63zrv}LQp5X94 z9zHW^zbqT-9Xsb7oFrei!;P(;zN+bL#E#Gz5!QV0_+)*-U7M_OLHcNq_O}Z3ycD#a zHH5(=@)JC?>o*wV83;LU`kv3sEsj}+EUE}|(Sgr|Nj=x)>2<~uhSZW)jcv<@ZazQ2 z#wWz25{Z?{%shgog_sgZfL7wTff=I{{W})mBL664XxyTKAK}A zizpM!Wk4SwN)vBRca8SRA>0gCn^knBdPBuX$K_f=ID(FfB%hHa2>$?M0W>L;SxF%X zgH<v78# zG6Ix2jY9}x2?Yha?(sJ^GiBc1B@2~EYP%_NtF&bfI206?)JzacK;a6uFhDG@gbQgf)jc88y2J(@Rfl28<9ZVcZzs1K`-r-eW|q$nh-h?(yY zO#EjT0&%>m9jn9j0L7A1y}+*uDaiml-qFin<00&^yUpHa)Vp()m#A~<>WZZ;G!2HC zf>cQ2NC1O6f@B{diye+ZHXt_M3-hgg%PeC&T3L5yArKb55IWz{qnzYV5|Wa#65@hN zh$UR4_}>2j9}Oh(k%kaZ1xsH1tIey3#Q;q*6|WyQ_Rs{4H3NViB#vjFkH^SsZle$) zBq6h0#IoTWMlWu>wEF6Aj%WO4{{W==LWC82=B4x~_hR;rSA*e-vI>?Z@-TH7j-nKK zXb58G;p6xAFodN+Ls1M!LgI$vHB`C4q^C(y)?)siSNP&#m;qJbL_wPjmQG-L9{4O8 z(k>F11e+P;0sGh8&jK?kb6O9GaUlStrK(f4?$9al6a;>*WDO(7`Nwih94HB^ymcn< zA&AdLQTi~JuTKix6LH@4Hi7g109R~DhJ}gaR^js#u*#54`|sYcK=Wl%MBn$HOZ)L2 zN}eAP-Y?>L`WW@5#}^nnB#GpDf%iX0An&RaQ1)bAkpf_MrDI=>JM~d8M_{2MU|Mn1 zdGHN&lJce#V5pc<%J3i zXtL*$;1@DHyR&)cZw(?ap}b*zf}htOp3!ZEf~J6m zaH-6uy%fWVu{m(8&2r`f3#wgpA_4%FD~a~g93uB7KE^8coLOa;5lDGW*%24TB{DzAHJEK3I^ofVLox*2M=as73OtERp65--o&6iMFQGfvnP@WwxsrT7U zG=uzrstO)})~4u6M&tPY&9qI2_uz?H7GOF!qBAEDh9*;)I(8MN@;#YpEM@fw<0%OP zOY#s1=kd}p)P`A;>>||Cy?NPd7x>#W(n~oqNQ4q;VVWB_egcr1%r>Vo$ui3KxWd%n zQWx5oDFR1!PcF8TanBR*u$3h$OOko}(_4jDd3RftQ*Fk~ zQ)g`zJf|2;ChG5YV5ov8Q#wwgOiKD0^-!8wVg|)D#D%3)@d%z?)P+t#vUT}>e7c6H zAF62!DRBv5$2bI@BB(*(wxq^}{Sd(`(xiIx_0BZ7Qt!$DaE_Xqe(9`dyxg-r{{V(x zPEVHAIad8zMLKDzA9ci~O8`n+ZAt(G$UyMd5_d>vqR8*?pIx%yQb->Z)lFw^k`C6kACG667{`6emT&@E5}!l!X&YLue}RTH7i^ zOS4Nfvk(a=Qy@NUH5$V=yuHq5dmU88b!7!rJ1TIK+f%sO7G}jNS`rj3Vh@)rRBf9m z3P4m*L3gMKhhLQ(+c^4j5{tWgaS2&5yY$W1Abci)2xB}L66M$ypi4$kII49NB<&1*xAA-70RVxyqGuwv7J zPcH76gKn&p6-8V_Wq0hAKHzcYNmw^DQIxA7NAsv zB_yZy3G^e7#oh)OWl#qIw7FE{=3XeauLr`%nM}1(N;YgjdemMR-)3K8-H+Yg_qK;= zU16g}y1v&eVzH#HOrf)k|qTNhY(pH5JO2B9n84jD#!ZT>ZdFM zMotB<7wJd24b%HTYlf=9O|bs}QK&epXQiVzHpeuQ7AgZR8jhD1(kkombNoE2 zeZDs@~0%;uEl3NJfv3EQ&88}(KyW>YSRuBrlT*b zC`(I1lir0HBLr2;pzpmRjb_aCF$k-WzS2rmf>w2tJ}0M`h{LJ2RspqO++iuq^)c!a zd$#HD_w@UmNWvkV86BJ_!GlOa!ZNFyaXE7I&J3yEEV$c(Zlv*WsPiM|%yQ>!NSa_} z6etK!-5guiBguw$rX@itKs=MaJg=khgWo>O?#p_vH=kFdFK;|u0}Lrdf%%9DLQ07O z6rus>IbyROVY*hX17!45s_2@oq0249=gINOa?Q=!zl z(zudBkPL-M1YArL=_47PrevChHc3M zVmW{M%x-HE@JKk4g%|$-w8Xq}Kf~e+tiSim9Cv5qw058-&=u|q(_8C4SH;iN)6exD zRSkVp67W1C%H_;~<}YtNFR7=W>J%vQt06OP;2yo}VSP+;aVRf^23SLfR!m< z)HKvQ52Pk5e^a0Ohsa>kRPgr^98oN@dYIRg1S+|pKvH zPnR%;lfp}BCI?CMw0+p)A2;N9#{U3i{B%Y3yU`!_2WrdK(Fs~d5F?AvuOVaMvXEQmI2A{NbMdo;0N|cV!mlMK>5j$%&LR6p4GSy0)Z49kQ zEbx|!l1psJiGXZzFANYtHzBwl`!UN5Gem1rsDEs3QBn*u4dSu1RW+_Jh64L;z|h6=FW}_-qDirFZeU&Wpkf@G0Vcg zw(F`Q0v&gjfGej>HS+MrhD>3&04lPfx|;Cw<|73(gknrrG)sqaxn%Ux5}D3yLR>>y zrbf3jKes{CoZ<;8NzAN9gj?WM@U6hEIMkgy53ZW|VWC24+I6k3Uq}9 zGI)@Ef8VFq0fv+D9Q{mWv~h7KqDB0G20r@ZscX}dh%O)I0+9Cavjna;-y@eX@F03~ z>w<5}p4W?MWmX>EjpYE|rwXvsQP$V^c-!A)zBTFHG=`WWg8lhGaY#w;5p6!B3}=^* zFO-EF*G!%pCOz8fI+4~s$}`HD=th_H{X&4iBn}rz;UB08x#j-(k32=5TiDf`vkD}B z;I<9@v(F6$8!B}~2yPT*NA!*m4zfJK#95_uE-Y!^Sa1oTq&d6D+{Vys-elrl$`phG zW*ATcH67GMn2x^r$1_M2s6yT4NOOgaJpnqQ7R=~+oWwKkY~!sclHUYKR0Anc)IqrN zk30rx5If7BUY*_~VeI(wg)){3=qPeqsN;AodoEWH2n9xa*1S%hdh>}o3=Vm)grozj z*^LNoTOul}ak7<{aTJycFGX=@{v)xOfgQW@-tZBSAW-^dYhz?a$Co#-3$9>zbLUw0%`Dzq?UqOJ~5a&{VWbEy{0mYFtLqW%->-+)oQ3kgj$D0GK3CDL%Ry zPs@?-+Aq%2F3BZP00;%pU4z%IaD(4hwoX~QwK-!xlFW_jsQ*5sRDHq6NBW+FQQU3J{z^;V@)UufQPEmg10QO zWYoKm*d2?jamosMRt0KQr&CsxzDQ6ZKox3ZNuFL<=u~kL=sO&^!bwR4utIxV-pfaU zTOqsMnMTw!>p6B?RZH6Unlxegpf(Y(}i)*a`msa#5yD65i$7a9dX34S zn@-M79L;5BB-e(OUr3>^ZQo}4{{Uw!wn_ zPysi&F@npm85HiG?Qpc{f;_`%mO+X35~kX!sj$;3a2L7Lv9&u+y8WxEQxt{NxC%_5 zl59yG-V$t0h58W#p9snfrI2)X&3fk#iZQoDp-3zc8rfS~@Oblnl^GVw=gsX#$7V@s zZ9d!d%VeNBlG@VZI2MCp#E@)cfvzH<6$D0~zz#MB9`OWGDipVy5)bLTb=M0U{M@~x(Kde-7^MvL zM&)!$FI^rMd#~6nzZqg&+Z3|BCv7%NI2YxxmqvbdNsJn*FF7R;#;a0^F>JJ4@ z=wa^4wt1TRcQUl0hS+_#8&V*5VnFcon{wre($H`aNNZo+@ylwn46d}(IhCo!Eg$m8 z1dB|KN52KbA9!CwnsCwhL++V@%gpeC>RsMs>O-tk(9#9E7KYskOoFgdk~s^Id3oCx z5+k#V_s$D00h&fqQIu&$90!|T6K2^4W3hX|sBbh_ZG~3$Xj+vFl$Q!n0VEEWn;c4% zu=A&&^3piK*~pxzsQ@S>tqLSxk`nyk3Z{{!4%ErU`hbYNo%6Cc%Dy8eg ztxYnA_h)JqQB0MU5-uF#=7j=5zb?Kc>4x7q2+Qr*V3Gv2_a$q}yqO}mzZAb<%)8se z+5WS4^a6t{+RU>sZoebWUZriymRv{yY?tG-Lr)l#rCcH|BKRbj#8;;XGJl?FJJ_JsTgeX}<@{%>DTvTyCK&g68}6}35@&r&L}Xry7Kx~CM|a#zCI z6t$FsLP0v>NXG$B!}ofeexig2a^WDJMNt5(0UP-!nZ$g>@t%_{)iKq9|#n(Aj* zsiLTHhud|shZ<58QlzOW0ZAcADIkDB1Xzqn!WorSIjW&Y>k;T!(&7pi-H-7hV)4#k z>Cg7IBtaycDBfoeZgF9L$~zaDZ6FLkB2Ws=*d3bl-pI$PAvAZWpPWtkcoHB_hJ~wMKNpI5{C#t}YQMr)+QOZ+B(2Z;3VW!UtWkf zm$OYF$B+Xm{m6m;012RbNBkN80P_C;-7InenWVb=#Q>4SZUl%uV%}bd7qR8AiK(Yj zUGF9glX2i(C{#rt~YUis{h2On4ul04YV8y5Xq&yK!dE7~#6C;7O0L2!TKOHL%BYsM_U+5WMY zmqOV<;ZFfn_yB!$H@-3cDnb{!@^_8Tgc(VZ2sG#cT$-6hmt<`=?JH_&DdFtXX&3#z zxW~=;9x=b!%1eLqe64|{DM~;*%8@Pcxx=cB$c<(`bN6P0Z%vatD;!qu_{BYkZ;iG0Q=4lvf!pD zBvI0~&Fim{ELDGqnNWg0R^e!$6cJ)u`rb>KW@nLZ$14godG!lajusCbIOAMNO3Dcd zQCScb9Y;)Rgxi>M02Cl$Rzj8JYZI_BW+4ipZ1e9o>fn1eQq^VIigK*~04J)cdWA@J z)h$J6C}?r4D^Ue0gE%yXp(mx_8@4orOZdT9A~w6TrSAE)t?*;9|=Hata35M>Mf6~^lPpnQMVrW$N1 zM5NIHE3Xeh@{plBRH7hHGbdZ$t|G#Ktdct!C%n0iUL5`r&jyvH9(Lo?_Y7!w7+6py zS#a{Fyi9HZ>q;V+v+`6nR@ zq3Sq1U`*?I!X;s+#5Ge?ZgqObAt?kH)5PINTjwF?^8(6Hqyrz!-`q!+zA&M0ojm?v z3(UZ<7gQdwNYXx9>*t1;F$J^b4z@gtRV9 z5oqQ_8yjLQ=l~?4x2fm^O~M~^fs2K>PMtN5i}tVC2Vi!~DN#m8Nc(OAMN^D7+AOGb z;hWDX08cyOJlKF5h2)+7?;0JO4oQrm0x0G0VAd9jn%~diw$%kiU4F^ZuACPgadgQ? z8%i=ya^2A!kGSGKEbAn}YN&>VD(mqU<7UI%-Q6XPvsa3++bzwzvKjS8Lzv~xQ`fMr z9yKHctOEpz0`NS1`Qn=!d`eb%k@@Aw(~n4rg|hBrfI$O#uG)6VT_p#xKI>_-J^k!! z)fBvK<<;n_T9ga|g(M)7G=c#-d**Vxek8!o4E8r~OQSk@rqgD?0!%Y3yUV(yacWRK z(u<+&Gi6i}vnrzs1p!PbB19E5f;o7OUp!zo`GnK6q|N}K2Uk)E{RLgehha;Fgb4r? zB^V~JTP68Jv$Hh`O(ip`Nd=W9R-WmQU|L}F(*ltFp-HV3)%?;eC7x2MY6uI#IA@$w zSK^_$;=JD@-en_2Co%?#sn(OkJ5aDtR6w3>rIlqeq{S*8wbRZO`@YLFO_-?9Gc4sv{-4?-vrtgtQbZXSxC2<2)3beSzy>}a-2 zFvzRXP}8-}1|DsfNioJ2q$DY_WQ79+dE!ia2l4%b4P~2l(xn$gaO<9NICkklAa<`5 ze{A>4q_zMuHIO;@2^eBf6O?XoC6fmYq_Qd-hkS()lvjw>o%(F{!)IZN%;BXr7E-0t zEI3k0fE5Jro=|kx5HU8BB+0x(rF`>Tz0k%>Z}AK%W-{*pRZ&1aeRPMTV7HZ&?e2B? zq*X1-5|9$=!e%)IYZ{Ade{Nfj9Grrti&Nz1$eb6w0c78 zFU~0+&oeB)FU%;ySaiyJQWgSCm;nB$kO92(wlDU5qC$b01s_Ajxwja$#lxK7%y(uC zSc8^i1ngcpu>4KxHjgvk5Uxawq@~kXVJ|LD!)QN(jR~(okKd3QUxzDq9*zDtI%`2!X+sbO!+c{9{ zW3%kgkVsO3cvC5n!cEDvMhi5u5K%NsviasOb&oIq0B7S2UFn3=Ay5#mbWd}rN=mJa z-7f03r*&1dS!*=(J5a2t*3#pOZ4Q@}DMFA53sFkD&;cSONySz^2r5QOP&U828u7iu z!5gwlaQ@;&Wj8K7hnUhM7S#J7=G8e9&A*?z{Yn~9WdUVUu$fOiID&axdEx~4GVW*rb1cEXtX0tlSsUcgx(DX*X3+EN3=9X zo#xQzYNwk+WGG!B!k~#Fz;RrgU_zsf>0e0SY&f$S6+s)Sxf{Mq$r?B3dFEZSRJFCp z%@EruZQz=P)Haqv#N4`85&rc7aJ}Vv*Df6qzWih?4>W!DZLHaQtgc^RqIj% zs#$QXs09FvmE6suWO$rQmuQs-+#N0#rK`a3ft(>E^g(-FMQmHr!WG`lyJM5Pwe0L_ zD$?uOF+*#0ZX~5$oCph^9w`KCJ#c9;<*1kxHzS0cSR1)04J@Hj@ zbK5Oc{Y4ETDA3q#ve8=PN`O*Ih>K4Qo`T+ZmnJwMeZT@4DD__WQaoSB_Fyq5n1TzS zAgd`rD!Q0?U)lN`-PS77r17C5NZ~gcf<>eg1ZzJ60u2iH);!l~n93%Wb(E=M2)TFl zcH*$Ip=a_f_DB}t2rw;c{A63oI$`5AwqgZ-c~L{Aq5lAIc7~iNN}|ex9Q{P;p}$Xh zikapUD5}2)^SlGh=>GsWmu3{IWe%pMhN*(mz$tYvFymzk7nH0kgor#qkr+aw+Pt7f zTs^``loTXVoGbc|&iC;t{2sliw_Uw$vfJ&J*k^XXWVWX~%`@s1Y?>X;Drl&48t3YR z6_j+%h0?oC+ZMuA#l#4NfqYotVlY(9W{q0cIINohDKC&!_1DNEQYou?KFb^L+^sfQ zoNq^WJ4dnkR&~U@qNh2aa-B8w=}%~p4_`hd-b0ZMf$ zJv-wPBiWOC`3C6o-IUMY1>|;=>T;C+kl=MG7UV$kw>>cr9GRwEmYk2wTI`bq0Yv~s zyRv7~*!GvW?c>z#zU=ZXtIC|>VzVr!AC@W*+8?J%f=7s*2d6wy?SQF3OP@CRdU?i= z!{*&iU@KMs0Hc0_24_{(=_}(DpAe(@Buj&$r zezn=-B&U_Up~WcA{fSJO z)7mR^NBO_g1Mul)8*U2Dqy6K5>0&3RP-ruC{V-ZxhwfaX=VV0D0`;f zN6-0Cpd=ERR-$8(^&Xz=JhdE0tTA9tK*Cotbt$HF3oE=QPJbBbiBeSX_Yeq9Wo{|D z#?{5ls`efq@Akkk908j9LW!Tb&9h!xQAj&gBq2!z1@qzq#@d764l>>;hc@XzUiKq^ zTeGdd9wXpFVh#g)0M>yN_6yX9!ZX{r37o zO}aIyz=a^lAb+4OIYPib$eVjG_`6>Tz7{L75VZ5<$}qELh*82mVew#n-WwY8s?PDJUn|l zlVr|R6jbGiMQYSsS!J6QNIlX45)YmwNE{#tAK|#fOj(1LqB|~Uf<;{ZEWCBwvVS3a zBF?h{C>>n_sY9yZsc8!QyNYcIDkw$D;6zC0iE_^2fVkv{J4Ll4IpPxUiJvgSmH=mP z*=i$&ovn8_%l3~l%b#KTYNqR-N`tL9(zsNjNrj~*Eff1<#{z4jDPqO!_5S$A3va?A zViCge(P_kf;W*>YvJ|!V%=hL^^pmHh{dC2YQ^IFDsV^l9PwGgGefpbQUonLW5w5HJ z`X4hk{jtvcx%nT6P-IjtF=Z~fCsloG-IOOQ6A*23W;|vm@1`NbfJjuW0AIc3aRmIZb3?$wg_c~PrdE(_ zQ*mk2-~O-&P&t(FrNPLdF{SXK<*4F4VKq|O?NulImDfxtnZGVa%jPi&Crl_?<0Iws z9uWpGp>K?jm&_QUr4$7VkKgS2df`If86Pj0&+@8SNK!bZNdv9O0z7(QLRFjqTAE+A z_KvN;1+QoISL{_*)>P0bUzk!-ht|xaiNXLKrwB}hi}$1-99H9gRXu^<_V-&HtGW(= z@b9H`Jt?Di3Cr@DG%%6b1VYi$J$38##2LRSyzb9Hr;|`#k*nEuebNmHMMO5s+~jh0 z=V^C)lh@L|m#D(m-U=lK42X>nFI_dl13ZtAU)9@Ji*R>XWRI75MKOC?`wc5>>$5GZ z*>Kx6)X8zu+EN0HB&ZG@XS>sNi6#_ofkT4Cw+d$im+r&_Dp;dY^4D02zQg;z z&o*m(m04bWO6yNhyrzrgvH<}?NKqVygkIRdc(5xvEl(S*^{8`r*LY$9C^-S>Jo1i; zHeHHpdM8_MwZ$)o`$<}iae;Fu-C_J>G2|v=XT7wo>Y7HU4u`*QenRekOSIX3T|gMj zYUyd*DkJ;K2~-aPp|2^Ab^;^=Q)@v*M-dSx+tXq*{VZun$#^B=P0#k@(dXV7PEX}e)X%a@zcLEZCZy+pns(B@?40hV@ zBYYXVE>ut_He)7vwQ!nBSJcTtNuK&pQUFONeD90da15X*UvqTfO7MYg8&}o9nnJ2U z052|RfQy3>vnPJ7%-W`^wJ*O^>et%1;v7SE~wl^datP}tv=Icn`^r*sb_5VdD^^_ILi>j;YBY}x|b6nGKD271P~3t z1Z$=xNL?B4u3s9Mrk-3~l64Z4ktS3s#h;H}5o2D;pNpRHb}G7;FW!zw-qF)msk&;t zp?SqQSlbX1+7L{t#K{d_ zG&5V2=E$R}txo{U>z_vt_eTgF3pMd`WiryQGytR7j=Ee1d7JIurlG|` z+@~$hUpWOWFKI}on+^~-!H@w4IUauiEr=MC?-CQt_I)xGiwr*vn+{SpQ2{@TGuxA2 zs=;qtQk0g`vZ+xcLC>x6BMpM3#*6)Xwc_Vy@!7E^mCK?PCZ^)a$AnQ9 zbNj0Ht7Y0cC$maA`WGB!2tS{sb+iIuIxGl|N0spa&X*xdpjh7cN8AFE@`8a}l_*2i z7q^-lGrc{T+Qd?-qcLNFV}(tn)SyC#3>hNC5-u=VF;D`u4N|`ag1vHy`(6(542nPn z080@-R8&`3L+yT1UAs-U%C_@nU(4vWZlytahhp*q*6C1Jc%*84Oqrgz@BCz<=%rtR zKQriwSmf*?Y%--AixER*Cs&IK-6yraRZWzzyxTKvnsOU$4y_<2RzMQ9C?z8E#7qGu zV@|)?0#Q;KDn&VsD6K{4q)=p~eRb4f6d5&DQC1_P8 zLMo7u=;OCyrGWzcfQdH5-Iv6rBuYwAiB|L+GoChyhp=vBv1h;u7}{+t;NhO z+Y8}KjXaej{(~G*bElw>g`*O%<(EryJ=>IKv9JyWWi3^$ZrWd!<7kILNN}ZE@)6Jt z1Rg>>@be&Xqxos;bdA!Z4cN}J& z#U&v|nx)jGw~#+y5InHrc|izIL>{ZfpcCQ$0Cwil`L=Dw>b8?|JKow|+U=%UTSJ@d ze$c&5RYy?D!c>+XAr7{cr6e}uKv)0}BpXg7N_T;`I1Uu1S%>lOb%GB+9WD^^IndmJZW}Gi2)JOYQ>!CmlU*F)@ibo zwaZf5aX~6F2NBi@7V^|-&|*Rc~ zPcd>7qURPEa;OG_fO6i~^0ZVR%^{~gIMGJ15E6d%cPimI+AaMbHM1%0~)SF{w zha=F(hr<5=ZgP4i=t`Ms6Tm=-i2~R6{W%OIIUa@-SH9m<6y}bI0v4mfH0RIUaFFDB z7-bobu-_dB=LX)Ga0-Ss7CFLEn1=5&S9rz%5#+T*qnCXc!RPgwKK@$s8OAe%d zolimcrUN)75EEZ$Wf_jSRv#_fRVh`J@L{zrCyvpqi8Jr(Y3Yicy9nY+NEI1X0Q2MN z7JeU#G_xe6F;JEAtMbue;Wq(RAIsQjN|J;mNsuB3l+BOJOJa7tm9f@ zffQRfO>bhBik9NS5QE<0DJ1j%>~Xq``!CpB!nJg4(J=LD zU3|J+Yl(ysNrwVLgaSE>$6QL;C6z!{Boo0XJuAuyAgYZ%d;P_U1GuO zYhNzKqDSoZ?l*h1m8BG0Orp~%E;c_l(h^gsJUy*}9OA>v1_Dz+4-GqbiguJ0D(JH( zW6wx+PS0#M%Tcy}JC-R-s!c=gGFGsYa8y<$Jd7Iyt~!P|>rAUuuD_-7V>hDq;kL@E zii>MikSVoOJjWBpk!&cqm7{>I#gwEFp)jcv%L|hC`}2rcsXUV`!_<~JKT2?pc>e$t zkHhQPHm`Nr{@?5cHba+fhF?hKYiA~Tqxq7QDbp&ZcZEh$3=*RgVlid2l1LW#`{W)N-F4acvnRQ)i}7)>bx`oe2d| zdAETR_v3{M6IDq~I*JUc{W)|KuCuQp%J^HK^8Wzx{-HqHVwFKcU<3|az_j|0*x|&6 zqEN=*4~TJ4l~9rMtaVg=MOBC0O03N z9keJF*|@a#QvwGgZ@YgM$6Kc((87Z1gsA~RLidyJzlZaHV#`{o6uOFag$dfEg&>g~ zB~R3JocoQV=+S zgUAklw~iDjIi(%D^z9@|jyl_DkNT9fr6XC?j}CeqB0zFbW?|(7kbsvSvHAmk3Ht`! z2eTGSPngrgwF_<1lAD-9RVSo{SlYuCxZjs{0yLWdAj*LqR<d=hcMZ_sIF-uXdVg+WavTS1dRc)*H3uFO{6T75}_qI z*qsYd-6O4@Qb;?cH;>1k?vWqz4T;%$EZyCr$|%{T%<2?eec+U&sYJ;WX(Q!0!FZgl zh-z!VKSpj`hOc6j%es|X3spZ_(8Pt=s`j3BhI?V!g9?a^Z5NGnJ!cq%-wNH*Pv&}YNXR7Misn6;jOCU%}T_{R~$Rvw=jsP(Q(deJvqLJF8G`LG!~IKhpeI^zy6e03XC9mR;gyn&oWEopTQr zG5DzNFJ@k(%C@&-GDfH>svA`{bwx!^3KZ%|l|x|4WD*XX#yCTf=@|b27Vr!_Ij5I) zpn}X-GQ11Yw=2)CX^>|bBgA$Np)C+V?3@89Nt=?G&}nX#v=mO-t23C&EZXz6Ofk@DR2v8O>EqFLA<&g{-H@dBk z*u9#{HhUwf4KYgCN@^0K93>(ofJZnQYDY3KTr5BhP_ZbgI0Sz|oM^Fdq{0deyTAnl zhZGqwHEY1lsHexM!nQ3uvi7QiWkhcx5`VvX$Vq+G#TTG}1_C-}Xs&pyo z6?oXfQiLj##2-KjYN|swQ@t&X${kiql4c#V^v#ymnw2FA(AgYHNhg)3%Unnp!2bZ7 z$a_A040A3%9N*;>S|F{Na^oAfSGrfXm!8#U%Smd0J}Mu1Op=n@YYr(qYVN|2k`2h` zhB%4}5?Qh<>&^(cW>S)e2~JFGiKdYG?=#uwJlkSiuu}KX6(~;drSFz(yiK>M^f55cUw^6Sx^BeT7U{N zZ3cSjVHqkx%>sE=6!L)znjEZ&zkH zZ8Oe0UgNZ{2I<^N_riztqq7WlgThIxx@WAmEI0E^1stEMI3EO0v zSrCTZRJ-ND$!xG%Z%<8$D3L3N6w;Qc!gOB_tV`9vv@% zOFm}IeU0=nf3#!ap_ol66%GgMB{h4*b@r&;&h~Olf|D)SjNd=avedqZ)`cm@oGmo~ zN)g1P-3H`o>40M58CR)q(le<_>ahUr-(g9C(OEt7s@Ixxb-P`(+j+bE-*)x%HJMI* zm$M12t4p+tTA6H;3$CRDy#NR%@GX2+V(l{kM)RXmi?6{>apt~1;(vU>SwPle;CWf$ z#@tu5uKRtTm07dTQs$MpQ_d-FRJz?l*=-HWX$@3_lz>uf3W!h|0f1ob)5$PNuFB8F zJk6Oa7}=(mXq@jd(3(^lYkG09x4&mkZJMpBrFH7+R~v6CaSv5D-r`!-97HElM4bn& zHCqRYOhl_NGzcbk&V#S42WjvxuPB;aDW*^Kh3bowmIY}RbGz?pZHn4vT5gv!r8OKV z4$-aTcFm~&0D(szX&~|<0PBrD&Ef)L0iIPLBfAtihV!osw%`0e7ZO>f`dEVh0D(YX zl3TZbsJ8PXui#ToB`%vmAVleXI($YhWyBK&Q1r<6@mR%eehogyG}6onMW`UHGIizY z3kE9Me1g;Ia#EH50Bvbh1E?euukJi?(4JC21SzCcPlQEA>7dJ;7`W0NZ9z$~NdTzK zSoN{_bHkFLp;k2gKn#E=B2#8h>_<3Xiof9Lu-?#lX7T-^b`k2#vwN9lPnc~pxmyE< zn>A7>3`6UdA|>x_~u(V zxA{)*YO<%=t)|Ty%IbpJbA`H*QW#nkLSY0bD1f3Zet26%p$We4l^dW~ve>5~33sKg zl+S{dVMywgw%=lv4!D6Ov@Y=yQG4ojpWH;z?FvCFo6se$J#ECsg9c}pUNxtA=Sac2 z(5@t@2-e0l-b1e~ea3L2 zJS19hsn>7*J{Pgm;eL2fp}Woh0P$}B0P;hH5#XtjDNs5S9`P7Zq375q8m2VJNIXD| zIs^PhzP%#tQi-JN)1u#<+9&L1a&T6tPXZl1^kuwRcI0-%<%yO;yBr)0VOU427%j;_6;_BJ~XEYl@`=d&Ce@xiI42tx_V9KZ4(uBHG-Qgpjs;{ zD&0~WN>I@4QkKe;kW_d$Oc?~sbHp569CI+;l~0N{ zN-SHxS+*M?N04S)7eJ*`Wtn{!6?8=nBOS)#7+glK?E?$67n{%*8R)duMxE?wYEqx&7#7 zRlm`SsCzH!Yf5TcXZ+sbRk(s|6lC?7#MwMQa4>rpr&~}x%9yA=At^QFs63)pY=7dd zN4I+!m9xA};cW&_oaNMt6-?1oDc0)Tc_i?0cBv|bfG!P=IG<Xm+u+>}Qn#N`aN zBzfkv%Z70XJ)}Lfb6nSA>vKJq-iFn)Sl(u_moxdtsn%NJJRqR2i-ke~Fh$O|6xdS& zfD)DK{cBc&9kNmn-Dat`9#Ix^4{gojZ7%N9vpL3pn^!*bA(xc>TGSR4l7d$%<_Vi= zXvDl3Q31>m$q5uQ6JHr!D;n3Ah*C&{a9tq|Wkt43snyz7ZCFO6y{R}mzr>DDzG z{4rq}$-gP9Q(D)ywIRDHN*z*MaXd5nhPv8x`1zQ)!-gY_3VIGc#uM42OsznnK#-z& za{1}|Ok<;p6?3}RIsybdzliSfqMwN=UkX7?e5cG$hrDydj66#%TTr(6KGY>*`ZazQ887@lxJ{E}x5Bkh(Uk}zh?-B0dEcjwBgUHyuy4f}Lw%j(XhBoT;gt7*w=VVkqv@^` zNL7@b4J+whQCgnUw26DSuVraV$u1y{E5IW`&%>ykS8acWCjp~2aF(KPQ< zx}^{bh0mDsBh2_?3A9UAxhrGyel{jFxN|5$LKlHxoFV?s8l=whG=Fs{S!M#bzey=K z>1hY0y)j)hg(l(O@7@VAAzmH&zpU%_)8;uUR<$tOibC2~3DB7-P_z*u#Q9^c{r%$~ z;`mZezTr@#@yjhV2F5EN+M9VBsa8u-vpXfTnNxY~QWR?|8)2}f4-_c~QfJ4XhM4MK z$aD0MHSt*lxD!ampi#X-(QQbrDtazLrnZ)lQ@V$LN56H2WM>BrtDLUx#C`z=SF*3dUn#$w?-2pmMQP=FK& zPNEb+kYwPvQ%uf(A$v3%9GpQc!4q5a&boV`X*U77ovO~MwykZt#X(rAsi6QilmbHd z05Jf;5JdFliy%;}axoM9!zV4t;#O1SS#DpH($Q>F=*TkE)09=9lySa>(=Mb5?F41_ThI5!wK4;0G`@zN&PHv%C+)b8s7)+IDsWH>#|n%xIkK^E%Xkhg)H`z*0g# z#JL0yc*Dgu*W);QJmhaG7s;Q4(!Q|~wx96_-Io40$Fo~Iv6Skn*&155#dT(4%6sYx zNDx(Ec;QnvfNv9p5zmiHjARZJhFc%iX`zc&psCBMvfQ~jX+kMqr9#I7hi10c+$5w) z1au(59d0nANrQ_fCL0wb1<@?s2WBbz+skGXknaBgZ~p*0k+!$pLaOrW#52U=yHywf z1j#Z$x4T&`8-!c*qxY0yzmeg}1}PVmomQ>y9Mc@{G1*$wW{Kiyb>5YXC`-KngdQ7L5UjvPm$QQ5MA8R;GO`8X0_l z4-^gDLWQf(nfHmdH-WHwJ+t(cnY~R8Z0nS5yq8wAESB6&gz*`%HWT6^Ft(Wpl-cW( zufkPZG>d(oYQn_BBu`PEYF4~FqABK;`#H5cWV5pEQnYh=nq`)m3GOmqM`}+EqkxiR z>3n8!{{S(O$xZYB05!TsE61?qIGf_d;et*F?YmCF^AxM&SM9V!h}p|y67 z0eF*gNQ-I8_=k#vfLGv=L4hv*B&eiSqBHJyefcg^wAs?#QYbPym8mr9Vh0+Hkdgt> zN0GlyP+gyD+&~EhSo5iJ&XlxzKZ46Fm}7YeM^FkNLTs(lEYI23GTH3YKdj7XrBw@h znZV*!o0TLkBr5k;^EkHH_WVJQDN|gn%C=f`SiQ%=z@G|W1cV{{WdQk9XZwpN$um_{ zKFu>P!>cGj0e6)UkWKU)QfK+cqU_sr(_l(h{DdB8e4mTnJbU>+gJI%Ni#({IX2=v$ z5Dpd0#HiS|EAo&9THAZHN<{XC^Bq)gIvd-{LGUrug*D9GS=OTWRZTpjYS<Hh#yX_{^-jt1ty7d)q*-%MNK7DH32(N*G-eU4yDo0?X&%gP8gijlph z*`1-tsp|U^o@80RPM345mA;|Cmm4lZ6UD^>K#0KR+6L^> zs;a3r;crOq3j~55;IN{1&M)SoskLOZPpLv2%b0i*pi-Dcu3~3V)8k)=N79_=peDp&FJW~lxVp4M)ymxE~=hbmf-pp@}@OzvqTh{Op}k_5z(BNBWvb=&rF zPx!30{{VA|=>hSKJ>L%63I_UAC;tG$;SYY=IPYsZRW(vp$l?CsgC-_L&)gqd@5myf zUiujDbs3Jm<{-)KrL9szl(4SxnK$PLz+s^h5V%Co;u3XL>#5^$R#{TMzCSG|3L~gA zAoGu?)}jj31;HUk#K1b}K*EGbAh=3a5gDRXr`}LXq^i&km%WCZ=MIM>(7+%CBWel- zX;%qV#|fAM^CJ(tGNOESg%0M+IMp-ksQ#3IWZO<%B#$}y4-!H_P+Ey~Pw&vxHmZDwnhWK=aqU8|&NO$F+exP-s}gK&C4>4*nq5*1%HZU7Pk9e75kVDSS! z83trTP^Ug#X{K?1Z|x3A?Dx33OwOgY(U?^-+w~`nDSb_;xmsJ8D?Bm;P0qNPg)SE9 z20&C9C-;zX=2?g=hU`L)sQg7~6&<%9h_>A3x&CF9?x8Q!(lXUWK4Q&jP%hNE z6oyunIUHOgRRUwE#EqKpxdhF z_QN*J6wQ0jsSF~e$Ei~ilmu{K{y6uRV(pt zJ}$)D%ir$BZ64+*wnSysgKyo&)0_z#3NTeyEPO3s{qdwc2Tr zB?%;D(T=3(Anl6GvAn1XdbeE6(1}|r2Z1mo$0Bw27@RzqWFr&~d1;dLRlISHiY8Pc z!3wBYrETZ~q8Lk5=Gm7IYuYFy$i#o4ss8|Gin*o+WA_62h$VkDsi-hq;T>k?{WTWw z_v4w5+D%*a0-S;vg8A`)>vib_sHr@zZF7G~|;RHE&;dG-+vDTtthJ6VI&A-G%ztO^mk))gV2( z*1RCmt=E6WFaXr@xAE@h;5RB%D0;syNKiS7f>4n>y8Zl(`OclmI+P?6TX3gXQI%p< z+3`(?^Y{EdT=3Ymu%b-)reD(x?e9?JSM3UJ9OAbAK#xW1{63sb(j-w z;`O)r3@A`9hlxPXLGko<|zs9 zQPA*^1Sl2;hfj5d{4o|aTMG#eOYIg7npYZg-8>*_H2V2p;jDAJE?%*rl(-`KxAn#v zeWu#QF~$@?NJ@f%BTx>66W5@0TII^bv0CEx(E2%O(ghx@6y=Ac!L?& zA_(K?=sQuS*cgQ=ub6Hhs9ms{U&}P&97=dNMTbMr&!z2*c08&L>*r*1R2sZ!Fv$vs zhpjqshl?e9^BGD|m6fERrUHSHamez;bn-DlK!j0w*NsdNl1uW^3$oT6qRyRVmGGg| z0<@^WF?bSs9-jHLV1NPMAQM4#x2UfO?+AV1>+t}$zm!+!8zB4nXy5vri*lyz{A5P_=f`4tS%Z50!B)an5( zD}#)fw4M-er@NK0q{7D5>s8QltuLsH$w43^w|yxN6<*V1nc#Pe%-BD%k8^#_)<;*cP)`s@=_v>J-oBMm)%C(y|Fu_ ze~Oo8>FMb+H|gHuLzU`kYZj#>kUQW<4T&dQOOV_u9S<#g=N@a~{{YC25ubW+Dqdo> z84;Tb-$<)=cFFCf&X#K)QtMWu+V+bxd!29Z=j(|w1HH)e;Ca-0$D3{c0LrGs#6VVC zA!095osCgqXbP6nRK5_k07y3hBwx%y_4LC`9H1_0b2)kT@llQ9a?S+61tC zCP6rjG@z(8aUQpYw~TBFWtSFGW|W@0h5fqx_DM-t{#8R#grw9{ zJb~GVQz|@drT|QmPLazHaWGL=iDu%T8mx09^K96XnXwj#SNft1faqc9_MP^w&G$vK z86L%K^(6&v(q=`f6@=9`>p5#keFzl@N)|wLYExq(4!9V@0TGK9MC95Upf-GQ>^})j4fy zikhjV;&G>0RK%q+4Y?}!H@*S2Y_hqP3lh~W@7p|6Y~C-lV9Y?8bOnl=W|UOO`$LVB z?=LsXYLs4fs0WfmEjZH90g+(`mqB|7#(x)PF2#f1LzT3a+JF%)d%6-0Niu}Yk~zV~AB%x%S* zV%oa!WfiML_}E?H?UvKCNs(tYk12$qmRTX?l?HpDCd9!4&~2ryh}&eTEXXtK!!iwi zOl-U>`3@w(=Kv|Wps;iq3wlKA%X6HUbQ==>)V@Hm$vJmxRT%_hyr98yui|V z;~})Dn2@!~>gd@gLml{sicgA7X5MT0>5{gnA&1Q}4d?HIxjDj|TUkv_I;!0YstHGA zgsEg1AZQ5#Q=psTB5#vG$eGo>SJFH1nWn^^RwxPvQCAEo`T|n$&Qpqt8v6X9rK~wj z=}1~7TrM(rc+Utn8u}ZV#{xkt@)Z(=vyay%%8}@s9L7v3Y5|twl{xS5LA}JRAhiHam%3~;>QmbTs&E38G=aBo$H}cIYiNmFqEXt zM)`5>}*vb23bF#-9ho+6~j1!a0n!VY;|54q7i#Ux5+ohkZ%#3voPK6(h912l%eLsH7&Z5780aE zLex8J8S(0Qo=$zm8`zoRESSOu1bhETaLpY+!;!u2c}0H|AZR&Ls5(9(7lj z)26z)OLZtptf{%w6Q{`k0M%*6r)8WGDNtAxG|SG2ZJCVDKFchoVn7yfg;PrMQwwaC znyH1h6z3CS0))=6dG+wWDUBs<1OR}!;LLq}VwW2ZPzlH=B8J?ayeP3xAB*BP=8G)d zj(M}&zgE+1{?F!hHJQG2+6vywTUycm=t5LNS&kr80#g?_qS`U$mlJiz-Fh2bk)0iK zhya19D5&5bTCQaDhQDL8{Qm%LcWbjbj!N5=&Q(&=HbF{?lBM=(g#ZZpR@~{MYO}X=d!J^HDQ`B~Y8L9Vx7}?`)2%Xw(o=JL zK)+oDpyMHrw}~diCUN$u{DKlK#{45ugM>Fy;4shAtJ*5V{{U$#E~3l71y5}T>GJv- zRiFMBL=`mBQt;vSvD`_Nr~wiLE0IYf!v_90;^%7dy^8@FbF{zx8y?X&{{Uy&D|~Yg zcf$6fl==><{{ZtiI>!Lr;t2{OBgdXS&l24Iqvu%I2`DmV$I{;)JXX*79$tbN(C{!7 z8lNwy$8u2d_7Dgb*@4}$A6-8DF)B&~`G4*d2vb0rEu@pL5@z0`5@I}jmJ+1^8VLuY z5GHN+5MpnJEDs2IYB-NrJEmkdq@@W1jw~QwTgQj-h?C3^fzL!SJ{0uLrIMLhx{^O! zOr0deO}X2i1u$7=8H4HGxk2T`3Kg^K!=LUa*RfF9{J(q|bGuQPiaKWt2A!vfT6<13 z@BpcSejCq^ojxW31hs9WkVjVaq-gdmY&HgLsBfBGkXOntFL{~%&F{^5X6|aUCMhT~ z($gt@r;r?dq$we3K;rKxqD801*yj#H7>ttWABkbsE=1TwF$$z2WYq+o)*@c=y@vO1 zlh;;fJ14c3l(j{tRJtdF1UTYSNKa^qUlAYx4dnI3o&a){v-h5Uk<8N#(rTr>_p$8r`JXBa(I{ zAS^f}=wsAd5vEir9ZN}bZA>Cxu;Gm@ZddtR5N4_3ePTpvYxNJlV;y1sa;+8@2sB}8 zE%5XPJ`wZzbj0IRUUf@Ei1lhx1@AVAp1yJE{+>$l2er2+g#+mO+%}lF9uhirxbrx9 zKQ3J%LCf%_O`uwRKh^#bf=xrf!i0TRhs|FsK{Y&2tVn_;0#e}CdqocoNc!@_LWR8- z{DB=md?-+0gqsu7-g@CegvN2LNB;l?{{YO4C{UlJ{{Sz(m{6eZm-HV$-wG5TwfbB1 z`Qbu`b9Gb|8HU?rR21lNxnEsJOn}%@)>TnZ+d>kV(~pob*8#`;M|TihDM^?dy^!~; zew*IG+Z9o}-)6nsWX?0$IsFQO_nmYZD*UQAiis1d?{BUm#Y4+8}OQqLTRAe3nf-fM={^0pxL9jxU)MwqS{!w1L z-P#v(EZxRJmZTSoV~E%TZo26|&L})f6e_4Bx&3IqREzDG3Bgj8&^HVDo)Arwps_@S zDI!#vgFYkNXY6Z;@kzl25{X)ernN4}Mo*{NFck@-Et%bipS&IR=`8W~jkc7@E#JRl zO~!}orZRYxDV@0a_F~>fn0I3iM#g5eaFnw0ok#<;WVsv!YY8MwM4u?cDeE$JQ!}g@ z6}4`uoXHh!gu2m8gpTU8=6+vfM4EClCo@j|VA9Cp3ZE|1kF+UQnYUN7d753Pa3oSy z1omLKpyy!dNk*iQV~50E1|;B2vq}QzTCI;*zS%&K0A?R@4C;uEz4WV%)bBye<7=~h zkj`jxItq-pGt6n}YqKg95mI|~hC3w?5(JVrGeAt!XY--Yzg)RKV4!^7mmGPxS6 z8!f9s;@X}Afu*TTd(oHg`owq!mrt>L<z-Q~ooxo{w{4zhn`jxyDwt-ZLLNwVvY0gCY$$PY71$J*JSjMl zC3#1g_|DORhb-T4aA=fzxFfGP=ZkN4$nAF4?M8O<=D+&BcVAMaJhE0ATkA_GJyL|k zO|;T+D-ML}2YDY}PxJ5c&9@BW8D*IiyRs8Y6zWp#3>{`xa&4%l%>_C9tw5D25;Z+V zhy>hBVpOyoBN7XOs*j!5&KKF6jyUZJqJ)`+J zGL@MVQoR(S(&x?~hSF~ar)zhsv$=k6nOEmItB*Ha`jWL2q%N@zxCyw3N#dSUd)hSE z0aS&1GCc^b=;2l}yJzxE<2ue=MN6#^Dd;-GBkd8qbF~}XsHXg~sdCJrKaf%)67X$Y zHtHZ=Bx*Y0_dn(T0OkEfHV=s|AUoGoRTUt;9E!F=726fF`!}@A(JrRI#H(iLf9$|ySvFHmQJH7e?nh)2 zR={wU(y~z{B|Je%nA2Nf#YYhu`Me@sv`ajxHb6R6+k*!z6Iwm1_D0WkPa~z;e8Qrw z^#h;w!aUXVZoYdR3f0VqWf2kj1fOV9;t^avr1*sp$&FF zoU1?qDT@x!Md#szNkQz(%ye;QSn==VUNMh_i9CQzM8t$scKr@4u&~SaODxYR5-S~f z=UN@dA9-taF{C=`ip)-+@)52qFy_jkIrfx~HQRQqdH0%SJX*F+nYAHNPqO)OrTmR+ zVfI^33obYVicPE%S3W$<5;@{h{i{`!vs`l`&Vc0=IC$8*!*~$01yyyr(Utt+jouFL z(91}96m7EIE2??7R?te8(IkMA7m?GS#s(~)^L}IJ%cN{PI}>(5q1n>flg9cg-I>V$ z06qTz9ps=orW;GCX{N3KN|uzZUIT_;oi*{q3_zr<`|iieHCrU&W(Cb1s9Qdc7ONKL zxhhP%BSn7E=B`sXnot|dDV9Qm&Tx+Nf@}e}CdbQ4ZL(wqPyjRH_g3hxV`JevJkw!> zRW8&$T8C4mL>cc6i){AA`fS-XDRUxqd@a*3q7c{ycx+>ZCqv*yGCNTd6GT&9HVC0Y zR?8P-1c{~uA1k#S0c{vroGqTsv&uc6r^)H_H>ukUDV6uggs2dpFFG4_zd?vIO3DBU z$-DQB7lZC#O)8KAs;KGaju$vqY)-{xxm&Mh>u>sjbwyPVzBq)hUL_e3=4}yi5+q(v z8+kFXu`ty@ARtd9BHV6si6y&ys0UncC(7wrL#~yrRLDwJvZFE-KUKPN=Z?hWRr$kV z%k=?ro?d$CtUmi^xMlAPW9zA^nq6b>QM$B;4;q+K8d{PNME8M+5i$hM3nn>~%Bp~> zAR|utLr>wy5bpt2E6YH=jNuFH8@0PxxlOChwrg#jtf!%(uN3(+HA!&|CWtr^77D^s zG2Vbkf$5DGhWLmRN({}%Fx?NM#(%_oGFcT;g_DyVFMhGo-tD&5k7;O{sAi?NX`ZG^ z(Bf1G3Q)8Z$Rram4!5>98!pr)+(N&I=c(ab#&+}18N-%AG6X1FAcZ|?Y^x7OSCzV@ zrD?5Nl1f&T^$9XZD46Td(-%T%XFJoMv>gq9rw`Gi3qI8IVUPtwy>8BL=I~p*y_DJA zv)KIiWA>G%>T{h3dmivdH0Z>}jr6o7yk$Dk)Oljtu zcZ&_vD49dmRXXH=IHs|Cut-7sf>D;fr`z zaC>ISqk-a*0rMJO!g^!rd_Y;V_K%!*rOajD%_Go92B28$Zn&rN|DAH{)puPHf^2B5%(C19=5>#ie zEZ}M@Lx+XHLPg|W{sX13No5yxu46ma0%w@26smXc(jD#6ZCdt!Y_bf`GcLPFPna~9 z%VtQWSx8I)$pT3{ujttLa-%=&H7R1+ME!+#+x&(dW0-hBZVn$w4m`RN?L~q;y{@?8{$UJ6yEntpO?1??Bh<%O0#QrMjZJ; zM%HFqtgqd6cd*+_PRkS}6)AGHw$k$Wf*b+V#FZfl?-dI`xfs5|mPuq&FfEu$DJjp& z6yVJy=4ncyt!bTATv1y!j%v5>y3Lf?e8#%FXLh-q%8}ZK(@ji-g5g10)Yu3FsE~_` zc<3g`($EXg^Vf03h5SF$I}x2z5XgBZ)+S zqcAK!BdxHaDuGf2ED=Z;33)0Kk!bL^^e10Yu8d5mQ~$1Sp}n`H1t=i4cpW1jA)JJ2i})3`mcOrI-V4ub+!8b-0kCqBCBM3(h`iv zQ35GkLJ$;{LE+X#{nqmM^25l)t1<7&5{f8XmJB<7G;q$Rs1v7;r`KF}F$5PB&&!=5 z;D)&h2v>FpQn_;Bw}!ub@eGNSv8kXb$_}G3)IxfkueAwKm{B0>a~2b?Kb9E|N1=qH zG1NlqFHaLA$n($n-FjW;()GI9~~eT)+uv%v)@-Q7zRBDd3O0{67Z!0V1bqVyJwHmk2B52$Lvm7lw}Pj zot(ESVx0{jaDae>ix{whuY}?>Yjz-#fNb4HdbCq^{r#r)hbY_kG6pE#TW&a{2a(3O zyiBAJV_R#c6kI}oW_N*`XEo_HnTzQwL2$6>QeZ@oLZIJGBj1iDm;g|P{=R!d2f?p1 z>EE;yZ4Dx8^A{Z|Bo|4T>Zu1vKPp&}#VImhvc@hQs_ek`8dIF^k@&Ses+=z~|&EV+>y8q1Yd`l;Ek6zC@D z;VQR}JvsbsiaElD3*ULf$!3>$IO;NmhRkIdE?HVtRZ~#=vmt13p&Hs!EJT6mF%EfV zS(F>+PCOvQgeXByZ*H*X)jC2;%{HpWQrO{3atH!V&m*Dt`QkL#WtLN-a$fpD_IbqB zsJCwZ;0PPBnXOaFsAWp0r))nzm#8RO)B?XjN$)`xHs^z3NI{gL4lP73=KToc6DQhE z9E7a`fT~nDpkF#gPJ2S{+Ph&rt5dqHo$Z&%QJFDIot)P&ZI;7MxU#Z@DM27{Nl3lt z8cl{`NEIpqT{FzTMMy5k7)zbaoe znOSApm-tWu0WB$NNhDYpC!rYAVaQCXLm_lt6zi%HoK<+Q@*qUt?|}d#ObFn8`9&~$ zNPAg7zYL2ze`Z*z%W`bJ#naQ4-C7e@OhS{yw8%Fcjj`0Af)q!Zc)y8FhzJ;>Wd%Ov z=|_JtSgO_4R5b4!FDMlfwTZl`S{eBB0Fis*RFph>*RjvYd~XjI8gVcr{lL|EzZT}~ zA<0f>Ou80-sk9atr{V{M5MXjV&y3v$Qm0*7cbI&~<2 zX|)uzgKiL;+UJnv&rEkMc^^Xsa1{=T?W`)c`#+(kTRBSzBsR%P$hZfDi6mdrX5Qui zl7Y#C;o?s@UD<$OsM_h>-57T=Ml1VTr6?>RN}GwaksPGQzXz8M9SPMvB2=ZqOa%(4 z^GoRxPjqWjyS2{fo)t{f(q>`OQUX$>B{BxOMTzA&Qz8}1`pfqV6dlr&DTu%t4Nm?()%ElDm&awx2rAvqT1(dcE;NwA`#I!^6cI6K#AvFv3~16sa7UY7&GJZD#bpx1i5A;S;N4J+GYhd)hS7W!p_w>T>0%xRgA2Q=loa=Nfq8Bx`&R`3^Y7 z!`(9=BY3%HqZ&B!Xq-Ek_NqS8nf7;>?Gn{nmr=0Ere*cY8(e~y5`t_^j-E$MIH=-1 zW9iT0ULpP`0!`;o6sncI==o*JMQ>X?+3GyDhq9(ajha?UnqLTPjuE1CF%u_CdY(02 zE3!$nVUPhbnNhU@xq8KOZGA@0=NZ;w?pc?X9!viK+)@yG80rX25d_{&myvuLRub~5 zx?AErtHl2RhvDsbreiFwa@CwGwV&J;L$kTQa{GwN^6I+evXWF;O45|2HzbfO02ldU zRI`OphfCACMt5)i3oMcuh7l?XB&$Ro!zY5bUCbHG-OHYEP?i)zg49XTY(g46>@xDw$sTv&KgZnf{C0RVvwDO9~A~0n_Kp={cGhc+(@Wp0X9?nwjEfLvw6PxDMEi~0F zI(zD6*Gf zgrZVG0m77Q!v6q7aWQ9jZq)8~CC-?yVMi~dty(hVERvfx%$^9^RnZyO=nuFd+q$vHJTczDC7fQyNcNC5H^ zu9v`z5;BbR-Y1KNH`+=Ig+di-G`d+ADV@8l@|77VaOKOc*x>AcSaqigE8Ls3lfw`U zq$+HA@&htEdB%^!ylCRfD4KM1Ae-_z4EiXEKf0{{0A+JJy2F%H3MXlnQo{jBP~n)r z2_r*ma_2baX;HyZ%aB|D0ZA2^Icg{B(J_M6WH-H3yQe(wz=ao1@b80pVBCgFBSmPpIaHTJ$BZ(na z;&!4<`na0y#+Q^kjc|z{YUAjL9k0SBSh;K}pu@rjq&X?m811ro>(n4kw z+6bLF5;g1AJgndMMB>|??;({eWaS)OcIjTJ$W!YxOnpsFHQJuAGA1_dC6AUR# z#E-BN?0as^aHaV3{vjEsv`*vq#k(Du%!|9cL)#9^Wh*KfigLZ?>~3pJr(lH3buX`qj7C!M+rSg*}{bw zc54PW6q)7^o)jo>HVUSWpF5y;G%Eg{&07kr;f^oLrQ|B0B5YP#;^7vP0Q4iol(``r zfphK4nneAUZPGa?t0%Yz^s#sup78yU^IgJIQ*Oz5ofh0xB^1BX+*+KesZ%W!zUg5q zDP%3nfIyL;>lSGUG$d3vMT^`y#K0j8i0|D!Zv@+q*j?e|8!cb7c`i+uSJG9XZ@K~< zD`}$HFkmP2i^06wb;nXlRY=k6Xq|*fax0}Ookv)cZ0sKJ_J?EG&+WsRwjoNJC zS%Mbd<-?8eRQ4%zx}qB^i`;=Ymn_RD3Q!55 zoj>NEcBzKx-DJMf7KYfUXsS^2s~ws^5yW749}MQ`M@*up9}eU%Xio&aw2Erp6WcRI zn(Zb>kbz&7(`HW7Wx2D#P1M#h(#lOoWCvuBvLuw4)XpVJN&*cW=jfOwKR^!tu{r=|fNN7RZ5aVb)4v8+hf$E;u0*ifM}wHbJEeXngu%kM%R)GY; zjc3MW`b21OAyLG7!KM61caJG#3!QE7{m0$#JbIo7&_`mUi1mZCslLfFks&7LI(+%x zt|5w!A`b<%sw+V$(4@%mf(F0FChyCwGV zC|qSK{?`$|aawuCIFUEKIQHKT<&!@jvSd6d1PSp~R--MuvIV54+Ef6eztv zz{7Qm+plWOi#c+cPBW1<_GQ{7JV6ViPZ~iacv$Ln7>9{&efRAX_CdWgqfls_lhXH& z{29?MYCaMV$Nisq6KwITt5tlSv1avv;0nxtk(T&nRq z5AyFZ0UXKPR$Q9J{-A+8r>W9sUYO7Lqng;BNcoo;udH3{GxfD~E33>i8tlozO4P2VrAr{&s*Ky;6XmnO5{x{`8vg)Q zqoc8PF@oE?K>Zv<0OA(;$aHbNuAY4VMwJFnF>fMQRgBD-#i5jqyjI<^P~bzpK96^ zi3zg;R)&dJ8EESZiY=4cv}Nfv86{$x{{WO0v`8r;RRf1!Ly!dE-f<#d2gjKbLRo*m z**QR*q3p*d%o{X$Yw0w{h1L?%=ZRg>B`JX?PnMWGpNM;9tvcT>kZF7iX_*MaB}xv(JT!K?TYNH8ie#a*21a2Ca>N+D)E8QnOVR_9zFwvZTN>&iy3o z%SyedATE{V#d5DpDodLJU=Ihg8t0gLK|a*%*4E@ZwnkHzviZ#Ox7mI0kfLEWP=F># zJcQ|rvo`4iGu~@hi9P{349GOqz2TJ0b|$kWZB<)iPNLP`Lrww@XaQ18C`p4FSo!0w z7qw6Jx(_&mi-2VmH$$(+eoIj4WEs1eM$YX9+$W8MwM1z0M44!%)V5nHNFZtqf$D9m zL_xDpG|mbV?Q8NYlEgx&e!Dl%9IedW?bnz%9atwxHzr`-e7fo+LP?H8iR1Aae+t6E zl4)g?1k7?q>{F{{Rj2r6mN@N-UlB!Y!LV*j4X`b~G;R@)XW>l@!uEw)l^3$pDuYQV;1t zCP}nFJqsS!gS71U_#grlg(ro1UtM8EyZ-=V?SpS4q;E27HLA`YTW(iYH(csRXbO0n z>`D|kCP7VvSir)7OR`~2G}8o`7b5lY9CHyVc7JtS1F-pJc3qNe+izwSG;h-f-f6Q7 zd8M=*;wgYqRmD2n+~Aeh_3R_5_A!ZvI1<=^q3Bx0(v`m&sox&vm9mvvL7db-L)s-z zx)Pd(IDtnJ%6L+_n|_md#GEn&gugdBXqkD4@(<+y0LSOW#GYX)2P_fmZb{U%RW8`? z2Ro_HEAqD%^K?%s1)`S85ZZ!Ma6WTB_#D`Tuq$mj=g*HHB=KJbI7F)v$<~zSU7^|b zaj(l;-iA?AoVtz%Y8X*@gvQmh&D2cVab>)oriK&)uKF;l`lado2{rpg2 z$%G{&A^<2{fTyP_M0@tY*?rkFBQLagA;Q9#0q!4~IUl-2f!s&64%KdIxqGDEr42rByV~a}63QQDE9sqIA7}YD zr->o85K6Q$axI25JPc(BpJd0~Diorr0n4&;{-RBLFm3KvNxnSYr*o%iavJKjy_Zo| zD~W2SeSB?jaUSp~Qc@I|Nj&ff_5T3k82C7Na_>!01Lm(@TyD{HTO{ojT~RrBUt+Qf z_z6#GBwavx`FP>Ok;F%(4W|T!-W^9f_K_trt$?GKuFp-?Vhk$!sH;A0Xb zGRjo~cl~pUPZZjvn+{+IV0epj9W)A3CSLEVvW%xBVwRC@uvlmX0!7Fd8s2;-lywpg z*({Qf4Mf0G%f3E0caQ?Yj7xdn+KC=Ho>0^AG~4YdDoIj@X~b0`=EU51^MlSXAFYT% zP)JT616$jSbtc5%kO2jPu8D9zdP8NkyI+>&RPN>g_i4~xeMAyVO@g(kjY>jNseyaj z9E>0F6JQUaS%RoJmryTexXKB%889RykPr~`SMpWi&N%w~T7A2=_j&tEn(pgioy{`6 zhRbSSqo`MBQ%c2MLrN)H`&A(!O{xzHNi(LCK9AtO2DsQn?C0@X6tZ(43#XfS&xr#x z<9NF84qaC|W)aMD9qm(FQvTt#>^|pcrv}TZ#Ys_HpYfK@D5#UvjZO5&q2Ty$_-tG} zR88IG=Oof6{{S$)ZetgRw0%vYNl8*n`5*EboZFJZb!t*%8J>inzqrQEY&8fUjYkgr zqX1_m*eZ^VmMLEJlr&04657vA|z8K;f4(Lb;N#qDW1A+oK z5kjG5;|dPj0Yiv*>u;_6usBdTw!ug#OsK{AkKbR9oTyMBG^F-o(Xf`qVuDHpg#p4;OaMgO04D>d5Muie24o5gy*luvV$n83dw$QmS+IGo z>nbSdwnn)bC2dtMVE+JO!jPrWxVFm5ibztI!X%q(YvBeUC4@JOgbEo z^zIdE>c1E=}JBg$69BD5R7ATIr^f>uh&28B5#x^9c$G>%x>DP_!ugJ{-B? z3`m4(*>Ux$AnJY>`;*~?3J!AS)WQ;#axv!^P@wUbP^VCm41|b{XT2Wt5$?i=6&y#b zb)EkJvuCl1t=bK%+UF_2G|F;;4yhtgUIi=fL_{0kn9~sFsL#LLTWp{b5Ia+n-`n-s z256?Kk=UteLE#OgaRJ>z0MdGc;p>6Mr)QNVQXl*C&x}F8Xo}jZN+%wmB)P<4DY;Mp z&Xa#3=Yh^bR8(I>77|KG0S`l(<&*GuhM$wF&a#B&Xl+V&w@Xj=NrA)yxpMKCgNSn% zJWpeqaf!Hcl~gH8YBWBEHD}yec3t^J2;vqxD3}%lsT{TO7|-~cnX7M=VRmphMYhc} z!f9EQ0)n6;_RzuRm5M4`A;hEw{{Wrz&>wZjG1nMB5(`yxRM3q*o-`XdNmu@5kcd=cUn&Zgk1Nc{y%e96BHZ5{+Bb(+hq-fG}-|Kt-&VUV_OeCSHl_} z9o7vb?g2Ni&HJ=EJ1WbWjWdDerEVxmQ+?6To}T&gnHT~bkAK+f6Qr0pQJqH*ZqUL* zI?8D%^Ct>fS$m0GN-7GsKxXFFA1pWpKyk{N!6q`D@3=bLULa@UE3hYdJAfHObhVXL zv{}dJC>x|?yp+57bfCJmgsZY`DG;=a6@)7n0*B4Ny$C#GE8;@g`wdFE zQaN_s?!@L>ysm1KG;K*+^hjB0P=yFcnMr~)y}aTvtJ(2Y0e1q7XpPZL2<0AWwd~{R z;xq56RG_7PGk9P!?Xsd?Y{flES{W}k(${b8Agm;iO^5D`9c}-Bt=^Cgs%u+hqYI|fPcHAHZ6BaY2 z<_)c`5%0f-jn)V7&76_`?oz=}IjU<;3Sh_o0EK^LvTV(ono4>M!8P>Can{z#RG_r1 zLQD%1Afy;4mLy@L48mnlLi4p&E%1MYZ0xvF)mTv~4@Q7H@7(=<*zWbMhoZ>Qm$8_S z$};(6JKZ#)BZwX`w>GrTn1*fv` zhz(inhMmmTGnCh@8Mbp%LW-uUSZKKVgQ*HF0F;>sn}+zJ;LPG_+CY?z<@-(i+_Lpm zG>;we&jP~0#92ZSGL)9t>ZdX^0`2Zgv#ZIrk#D_Dw#twkGNP5GCx$wR zzN3|9E-0N`2hN14p}!oc(Vv@T2E$ZG&sFnV-XFcI&hlD(s+zGhO+QlIGpeUxASp#) zgtx*3r~;?}>CcfkkM!jsR1~=*^`^WQBhRtQ-)Yr~R`gDO(G<47XEWWn&8Vtt@|Uve zG?vStEcR<{3I$|~0o(-ZCj&j(zEN4V?1EYi$Uk;lVX=E~xCn-$EfI4-DD`h_qjpS$h z9Z{RHPgO~^REbm6xphHl!&OR9R?-laC@o7M2mo~GFwv>mKa9_Viy=v-3|t@=E5wUw zl8FB8Hu3ifwp9B&w6qzfL9~qqo^qAYwwF^z>iA2I>%!PcI>KVw;YPwkF2N*{l7*;1 z3c|-#cWs zuD9stqEb)dE9}*^dz#wM&rnqD25+*vL&S!v1Ma&>%}SLDE5Vf}veSpblvXS*Wj~djS}Io5W0$q^5+??yFTCvbusxI}Np)(fQlcWn@j&uC^|=_J@fVV0cVvE}imHb_W6}QrkN*H5n+!`RoK$tKDm%I) z-`(G|3bxH>C(UWAGbihj1=5AQkhKHBDo9WhBE<4GiN;e95iwEg@7hPGcz=lfc|_BF z!4#=KCZ3%k&7j%pe4Wai(xQ~pxmKgVN;p(6p#(@A^!S_v%_ywEbIzUd$i;!&$C%22 z3lhX%Ce!8`J-ywATfCg>AXObImzq=6r3zI$MQ%FU)|9OygcWfqB!CF{MjL)Y+risE zoZ~O!UJoS4e+(%ONDHbn5TB%Fmj2H^!<*OI-RV+o3z;=W&Fq#=DA;c?%mUE5ifU&N zrG&KVQl)Vr1O+4j2_Aw_XkeZX^)MxM2owZBDdtC$EOZ;N+pXEXk7c$)qP=dl1xo;w zs0d1Wi@_d%fhJgx0V)_TdMs(McKlF4nsbw%cJ=8{=ed?8G|5XJ+DQKZ6xaeHenZ6G z*ssLe$Yov$Mbsu;axnP&O!EiGfpgN8My|JjHCdzibt+JoRDdp2mBAo%h}1~2=5ZhV zNhB2&(M{1QRM(tRV@PKtp*^ADcl|3((|c8E(lpBL9u{Mr26_!8hy4sowdYR>SO$#S}lq0jRs=rYW?hbn6;Ybu(iVTyK8ryWw>eMxN! z?9iwRS9w4H4w#jNjBx-G12INGQ$SOpHr9|GqjLsQcYq*JlsSOo?DMlhscaRfh_O@~ zLbRLlTOP^bvf|5$A>0ThcVIas2O1tR<=!ES^(LNkHe@)|o9WP&v{Apc5F>GWawm%W zGbf<`0E{+&%bZ`w7kI`~g*pT&FE6@UdJQs)EsAh<><|jC)W81#s>If<(#sq39i6L0 zf6Vxal4JaN{^HS%AJRYR9R>S63}gPF)2VvtbnW3fa!Rx)%3UOL?-BC%>Eni*1n!@- zP>OQP--Iuzr$hRT(@yXZT3J#pBp#gn{+{RuXDWW*K9|3h&_`Rr>eXZ0LA@huN)W1!`%n8HI;DVj$kbkldr z6`MDdRPBClyt<8T3Yw6&62esqSpbrT^zSIxK(Qip$3kg~0Hm{3bMKCk&fpg*?GFA& zx(%eKpni_OWb+3es;;G`%j-(XMqk^$hSuZi(Vptb0c9kCY35I%GAi(5{5!FUdnhJp zO=-=a);bXOUiM7bZQt&0ziG1!;;sFne=katN|Y_+hl6CG0Fa`u?nH>Z^Ei!(Ds@3+ zUV41#wAgUkQh=2RdU>TURD{0bcOSL;Q@kqN+iNlkr0m{VoIO??a-Gzd+6yXa2xUl0 zl9vDqJH4{7_ilgeU_mmONSZ3N^Ues}RKbfb^-ee}7JPC26sH75OlnLeX#Jo9^l8R{AK6LYp?$w1d zPBL0kHEk?wT;Y;&$I?uI0R2SCGBx${=WI#B{{Smz?)~Q!Ca1rTK^i-L`U$)$CV5}P z+Q;X79%lqqyYI--7|aEI5A_OMy=}Uf3bnrEe02205FVF(GhP>jN&}s<)Aa{D1!5Bf z4SH$J;K~rVJj4YGh9i-UFEuTc_~{X?uQO|45O_jo)WO!s8ld^<&zK+U4=qO#>k1KV zG`7~DtKX&k0RBE4=Fj-^>k0%1d(rUyj*0gKgneUW4|o+Gy+!`~I^jZvHdv`; zB0&Kp`3@^z%L)`4p+G1>8jfSA!i5-AHkT>tKkAd~Z`+hdJH%oHz|Ic zh43UYqM!@{rD!Lq{k;V}2Onb^+ynzgZ4|>Bn&lMRL|GLFjVMyMlC#v<{c(*O`i&|6zrU~|)-&us^YYrW+? zPngo73J7dFz@cGa4v;YLl+%eE4iEwz|Y-|6dGB=KANt-K2Y;)dR3RUw7RCa$c=UI7n?^s zD3B-5%iZG~+bw#wsmFp^DJ1HB{wJIreb`ijeMAKa(Z`n2KQ^9x!HDwnkwgl zLOWSo6t6g*1VK?zDFGoMl49447Y`T=S#9@Fa(bXoDr*q7$qpt2(+~8Y-%eJfaV_mn zyn2ne+breICHDDps_AqTu#j=$k_mX6Al#mD@h?`?;u$7iVN`)N~tFM%Ke~6vT zOhsFal?P|B5V0lIEI1n~bBQV`0LcJ@1Rj1MThAI$H?Rm*X1#`j{5?U);B6pnl9eBy zl9g1l)cII+NBDa03nbbMlPcNfO6c<%M(NP=WPqZO&`CTqF*ftIDTpHyV`qC-ifP+g zK9v3)wc(wc43YsrluluPzOgWN+iX#@`&_)Hsq`k06}XV#;nhds9(oQy;3F1UDpNwC z+V)QKk5k!pxg|9OrBUHyWkH=Li4?B9rcs!+PL9r6bxA5V6C5Q_VEAL6`|Tqgvtw_}gx36(9{Vhh&&}_tTNmqcTWQa3%Ewc8W116kcX7Frxne2Vt9Nm+h9Jo1xDm>zp=Q zUaa-qn@(AfWR*1)U72mlvrO*_N|ICm05h}U9`PLcVM8C|c;vjL`HZf^Jl&MS^Vp-= z>u2`5t8U%i4oy{6pXCduuF2i($`sqpvZ{o(jwlYxh)61v2bP#oGiC4$Ts%xOJfQ^O zlm%Fe^0Q_lcy7BS+t#<9hx0{LhW?9#X!WDYh+vPg{r!I{&ZxKWD!L9^g& z^N=P4f|k)XGRyZijH)Sp0Yic+3l~&GQpfzxWkG=eZ&#|NrMqL7xMY2_3e&vx+WD%v!0$@ zuMJ+&?9D#JZ5y;ZFI37s`TY26S%1I(&o`hGOKe*?U z+ppMQ`#EJ)+jCL0NY8SkSnV+^(fN=Pv%!!NS4sHoiBHi5+SmX zr7MVl1YDRp<1@BQo8%!jPC#)-&EHt{9|Q3XsLO{bmSiLen7X%;sa^&zA7?$0w)cB1 zQN2``c4ueaS^}%<8d0cuB%~6GaSk^rm?i*$eQ}t^#zk_gng()oIrFtWF9Yd}B_+(N zNvfbHA{XT477My>W4+mKs+otGq;ELRSm0Bc#$?(ad$j^dVJ1QWGx~uet+CDyCq)!e z%tI@w$rSkvVqpqM>BOPCYBZe~!;2lB}c>0V)>SquDZBBzdQgc-)5)aV*U00e00deo0+oLCH!F}qgYCf-7%g0H8`e`RXZ+L6F_9k&S`Hf!EC!)MQgbJToh3y445 z&7hPI{vp$@dBkrD#WQ%G&JE5ou48A#JpTYotXXzzxW>vzO)3?w_yp;#r%1f}^~a!p zhW`MPZlA3!?qe1)zvezy>z|n9h^X9}P^AS^r$ZyN7q?jJr@N*Y?7*>-XVYCqXN3W@ zH`bT^RP#j1%KY5Aexq_=PX#gtx;NLJnth1S^Zo|=q~GyWULl+&OH08<)QqLomb9{D z>0qqxoOg3IC?XgHaTx`x{JPKF;eUY+#)jM*$Y}`eB#M<@H(FCl+@K0zs<(A@tu(F* zRTv}yl!3^P4<1~(;8XZ~iBQ6xWlu)3L(8;HGAl83QR&*$LLKei*R=Abmr>M!U{8RU z>SNq*h_m>7u0RBv0eyYSzpR0bcXJ47Q`bICK?TQQGb$7~@`~1j#H19Ug9--G>m5As z3ARE0on7?0aE@feT^F|LuUZ01P3j6bPC#tmn`71G;I2u^H`U=oXz%^Zg-rDdhMxeG1-j&0BHi5%W3MI zZB;K()TA{`SSU)AiGbaqaosu`8xsy$Wg&|?)mx&nb4Z)9{&Mnb3Q%`w>ZfxXF2>C4 zO$YTHkN)Sx!j}O5Kmfl^^&5{Uw#q}x5D6f{N@R!x^@*g~cX!c2qNPKX(61ju8{ZC) z?nqH-!%mlDe4Jfjcv27~#1H_Ik!wZ}*xO2jW( zmg!MKlu~Bu1W38G=se78%SqT)r8DhLV~$amn*;@CQmR{2w*$@tl(<%u$+UWoWA+12 zeh!5c1@wrs$yRF|ySy1uRH>IZMZlddbhO8(`^0h(6;W5=RN)c8Dj4$(&JZt?vfc^uN=`3KS!1+Y3{nL~zN0)ZFVa_u+?@qloo|3Ad>f zNm5W^`VR}6bRHV$IhnKmJp1k8A<76{91am68S?Oq1__6LUh(q|aID4of3J_zt^Li2rC-|#F$E-F&T#Oc%|G)S{{YqdVU%V%!i-*hXfC8Dh$yVix`BBf zxCBUor<9Pisa7YaGJ5K8p+-OEgUAzw3LKr@?Bkn3w7GQ}5~ZrDs+qRZl$9&{uc1Gs zGGbH-G4;YNR^rjrfXy)73gVA={TY7>&D}SfR7fouG$Znmx@s3m&LPmd$$$D|%Bdw-NQV>qUAnR8G1v&RN>3Y&m=3ycdJ2;976S1oJ%k&MSVG{Zz}(hid>BfGU8hPLW)pn+6pTK+>VEw{{T2?hya}p!1aQHpd7>%)wlgyEUL>sIGa+IQz0(O3V_vIAj}D$b}MC`V~z+_6IB^HjxMQ;Sh?N&u1;pa_`9qxe+B z;Y&vdj!voM?ySRn}BQT|PO{PQzCsWdI%xUA! zakJT^l;DLR9nxRm^z_a=ntWVIcDYQTpdg@n9LELVs*xf6lJhq=X}&C-RawZ(C{$9K zq_|LqT|sgZ*()FdwGaTAzm_XDj1eiEpf%v*DmGrlN4Ea}7V%tcJ)Z~yLhi9rg6Wkx z<(-P}VvA~a!#;OtGAd=)GO$p9l&VTpND6=_gv^C=8cs7CMCM7068v(yXPQ;?^`8&% zJXy#JXpfR=Zt2s5#kjqO+GfgeYDZ`t+*&d?JcyD zl&M7=B~lCyQ5@`WF=v>e1dCSg)(G|B%@`l${x3JvGT`Hh7^+MIT{61sXy)fB+h=#h zyqxE0bCe1%&*o3oyrhXjL36e|T)1p_r-bU5s&i?>UrKn|94OAs-iZZmg@<4(|_V|R4 zeBmLg6wM>rya!`5Nrg1nWw%W@EmEA|^9UVWFLx@^hzyGn*pz?YVW z+bUW@LE@4D0?}{|9Su;XX%R7{!3Jq%5{7CsR8^b$tTdU2w6;-@)Ymzk<#lvos8XsP zQXdT@Bqb@PR}#?*CdDG-$Hxjhug3QLYpdL8AksAWxT(7yH~vouWPmn zoUW5+T}$<~ybDb;EYgzl+o*CoGUCvJluD$i+DQWnEq{w+;>)zi0+69n&sNXXjCBR< zg?F}Nb+T5*%18%Kfm*8+HAx=D1ZUqS_6U3`&IOlp_^$w$tqfNyuPMc~mu--|D__UiWEWEZ;JhiDGbZH8Z+@zkz@p$ntzsgkJyZ7oP$ zp(x>3bxA#N9C?sc-A13ki1S|_*=O1&GRh3nl3f|{tyhjwMBBUAt+v~0*0QaX*?ikG zu3ODEU8kn0sUWztr;1u_a0aC61e1!KY;+SiT#055*2?*dn8a;A$L9llVUp^g1xr}- zvx`%xZ(~oxK9_0tYe!qYteLbH+hVDzdiIK)3R+S@N*zd8Bb9*i=TnWes)I7I32qwD zDsm4N{uSVI&oDTWnRS{}8dVnKnp}vp4cvPhZa;O&daq@+KPMTRYe^MOu#)j8Acb(c zg(hT7rCP^4XSS?jNdZWEu1lMLX&ddAV!+uj#Inj1%E$n)(=(Mh2$eZi6Kyb`?WPgJ zgwr&R1f?lVNC^j_gQqPqL5nGv(1lY?X-j(ZixDj243jgtN}#Ku%R|lvyTIBMrxC!O zgk1F0T%Q^7!LViB4#iN54xZoDXc(L26|0lKJ~ZUe6Z37ff}q+!H{u;F)8o)$M3c%D zNPR*I>v*3p;yL=5C4RLJEk}#HB0w>rzlNWD@y0GBgb}<0Oe!ixYr?7L2Ezw_Q(!?| zAu*pV)Px#y5y-@!E9iY>>538*kQ`IK8Rruru-jd;%XVuhez!b%s!CdFXDVu6b!kgY zGRjorjwwnY1f?LKNa=}NHV@Rrl2lS=0HdPYZ zO&;D|PG`DJ)8z~Jb0}L5vo>69M zqUn{R&i??Ac#=RO^dz(&N<*1-Rpi?F!shswG||nfhbX zu=WWsB$ioZ1%rU9GhW6_Bg^)^s%@ZROlSMIW-@~@1XqP>&z4a-eUPgLU%btdT_Ti+ z+^$VCssw&nM#)cTlVikSNyUx?l7sk*w|q!bNW){|%`qUCB=d7+Uux?XHSKk^+fkpj zO|w-wMpZ{emQ(RBF{7nQR^Z|8rN>esOFDk3^2Lh)!&baQGLoiZ?n=LyibwXJ1v`}6 zmdd23{u~B+f5sUFhQ0^O%K*h}3DWzk{{Y7?!2Y2A4nO?qvXxXg>#FSdy}We1=E>!z zCA2Es(>V5du)wQu*EZ5HkX-J>r?N;rrl62q_>+c!0i|p0J>nqjyw} z%u%*H3V2B#gWr99am*+se{~dYo3c=D1bOnc&yP96Ku{DOvD$~?t?E=FdI-* z9Z@{bhcJJ94sDAJ(xRRB#t#j(cITOG)hQIoP$ZBGbMTISN5=s>CK?gN?Z@{9m+>4S zBb#oWPNRU(ra5r8!^@llKQA0WpJgFc2uh-^oKoM^bq$_xx@W1|bAp-H&HKp){Pl?X z`>_&jluVXKs`c~F(h{TP2;75avmKGovR$Rg%k0+d=3t!Hg$r6ss-kr+mcdUA*8~k` zk&$ToM#iAjXokR}@Xw>P&5LOQa7l1LLc=aClV+BVpM8hDrN8YVS$D zn3CcXQcx1;m6#kvl=x3LILYm}CP|OlT~1}Hbz*b$k4o^Id1u3x#0Ienj@IdZ0@*fW zI?wVKGaQp9pyAAzrF%5X^7IlZU3H*bK}-OZ5-d1=Gm2bN#SUtA(wWVg%w2ivYS*foEwrUJvd{=Zhz?3g0Yn&?(*u;Dx@8hD@j;jZ zSMD5nY0KIY8}6s3pxGS8f`dAHX(^qqZ}M!gg;cpooivasTt z$jJ_NZK|2ZLc?m|l$D`8LUmA)^_Pb@-eYjS-hAsB417E^nN1I`t+O|DhBd~Lf~XV! z07{LC9}ZLFd|1glIIFjGA}euD5DY?*M9k5fuUCL=ttoV<1JBdSH1Wp-C?X-i(()2Q zjmaHk_#bt->D1sxLXH8y4)j=1CAzYt@PpkJkKfDR@Hh@2D!jS8C=TfD2|j+hc}AX? zSaSfzs<|&aQaCc5;6{tz4|oiqI7sCN-k-)MM7T)i6#oF+QlQ+Pzfm8*A$&)L1!ZY& z*Bv9sd$1Y*03*xLLV>jFDihQgKMO~hz6U?zd3uO(QN%#D#GQRFss8|9>j8tn48^;3 z6F*t`{4n8EB~`e>k7!g9gx>r|=5_htv;I8#LW6!W0F6&Cua7JURB;fa1ujIYJnuhW zcRVymf@-%eM~^YvV?^Qvu!0J@y-;CB!n4gp?e^RaQCHF)3vXT6%{&UCTE|=-`B*E zVln0O7xLjE8#OauPkX{|D9jLsS!vM>rLYe;RlO#D?Kq1$1>6MZelac!_U~&b@;s8E z;&>h5FlGo4d&BrYwjpB9C6Z@-(vP9+M!d@YY|`L}<k?-%&4J75djh2>Bysj;6fLPXn7RG2Uyp{m2U-eLa2~cXGD(0? zXmWnr(h?OF>Zp0H3b&Ht6qTf05~Qj?zc~T{vC=u>4)5Ly(5IRCiD0G?5;;Z#f}5(I z%0*CqFI$$(cbU4}qj;G{T!yo4i>YmEdW}TXRkT)@5{0EREwzwQpmXdoA~Z5_@nG9W(u;Cd$lajV$kL6Z?c}T_-&u1GqV)3qozJWI2l1>~B#@a+ zt&`+xH0ut|>$dx8Hcv6nwr_hm#%q#L?C?Vr*|J)9^3|==p3%BO@JInpHjV^1464^S zrrI!fijE}H(WhlwFN0{dM7H5|_Sp3V2 zB^A6P7Nn>iC6Xk`xG;4%;|mEY60hZ5Z_xx(?LIrZEYgXJ$}`G=zIw$_Z(nRSn{8^{ z+6}0ycR0+Jl8UOAN|aL4!3~6|K*>omFERzS#fAi+nmW{Suj$NrZ}Kk~IP*;NDxg9} zZ`&Kfn|65>CeG}IZf8=orTopLwA#r9NeU9;#)e8@6W7B_nJ^a)VnO^+DtDFNMMLMi zMCN&fvnow9lde^w=a>YlMBerS+1-NL4ff@^ZrM1~xl+=%DIKm`OjDZDM*v%|DJ_ms zCP}r;v0XM91<4M990azG;?@rvF9E~Zwroje69r`?hofj0G*+$e_kUEZ?r+)mY<9O} zb4;F}^5`s_y@vvwa)hBtQ|RHklvM#Gz9gGpgy05wrOcJ+Xyp61H;-J{d}j{}2um#C zfl4sc{{ULW8E&`QV>P7yp#EjGFHlrwS&ObyaXhx0Z6uQpkf2hJ`rxF5@j;k4NOFTs z<9IWF`am47+yW|#Uqe&PWL00Z_V#idlxEd6wEIi5?j=T&qa@7HTpMK|mG+FOeX0OJ zC0tGt2B6;yc`rAbX-Mu*6~LH;oMxNKyqB#{2xqeG;O#c`(VguEXHOX|7F}NTI#!@k z+m0c86C{8^Ai*MhaHApokHjUAlPPCF2ud3VbPhv3BCI!|nPwDO-F9Gwm2s#rt^$8b ziAk`X0rQVMA>TsuxAmCBO|{Fl5CSUd<K#bC@ns!|Lo6srSatUqH*hpDV=^;_Y1Y(fwGRufoWUCXMlZttrPr2+e z52OmKA>xV2yxrC}@5k{1-v;Hjl{*Enf%lktj*!wOxh^TiD4-5C_fI?D-KfF8$4EcS zIg#Dzc5u;2<6b8=#0UiPMie|(rnDg~knbn)3ihU;UdhpB`P(@bWDupND{1OlcA9x% z3+*Zplq9wgqsJyVc&0>QrrV_fcf$p4Wt})EE9Dd$KG*&^Ou)D~vRIxs;Hwiy+5Z6W zR#tPFwX_@R+t(jjTtl@PZ4wn!9#r1Au**|i7ipl9|b`xZBx>~lIX_xD&DVct;^&kLU4y_Uj$TQFxzA5o` z+2<~!f}yA<%oAQK02n~kE0$=alM;%RC~dT$fDjYH8vrG=AbymkDS@q`&^&aDi8(cL zb2J`DMfihBvxyL*4H?q3`S$^8%0hz5c+ipwK(b63)0jN56A&m~N1uJ(M5lI?@B7vj z`x2TuoXsnFmeR_SWwnq+x&!X@^EikQAG(+9LYiraARsz=JRa?yRgybQ^VDuImr$Y< z(`rZnbFd~14-ecJgbAi1l^yF1DugDXJiluU4sh}**%a);i7Nj9E5sRrGpA3kE3t79 zASs_3FSfeGktvwMEqQ}fO{luGq?3LnN%RNHl=(rtPpVuO1(nqUdGhNAl>%f4gkIL| zbuA`Zy4UI&VwRb;y7TTjhLV=s1xr$vN>Y#lOsPZx8jL9hB=ZR{Wtge5-PVVYsZ}?E z%`~`ch)4*esj3m^j*lF`{wv>!Z4=B^ZcB3+!*9I9FHr3!ZpBJmLR+dy?O0~1r6??2 zNmMA5sF55x`j_!f<1=FcjkHQ-Aa4~0wZALBrU&^ik4q;09J2}#05_!p()#Nh#643} zTT=CHTdRJips8*t#TBoKN}H))9({4^J|D5+;1GxiG~ri8wy9kYk?q@m)CFcb1-f(5 zIbJpuaHe^KFbKR{0eehqtm`<^Nj?w;QM*olg-hBf=EgH`RlyV&30n6{M1kC1cberJ zO+%Pbx>*cnbdM!PN4W|?wfn$D#*iSy36iI{MycZFIoy5!0KQ(=G|JXqz5FnY3~l_mPw8nRtfYPQz{x;Jv?vbc zJCSgK(jdoM5q&TSP#Cl)^nj@v`Tc=}9D$r15TVB+7n8>KDZ-OvHaQtcB#77Vo`YOG z(o7$D1rTe?pO8q9qcPM#CD7A^lIu+ss^hwm=|4m7z)YmMAyVety}e-4%_)>PV1@F6 zYMi2#;$f5|5y>;@rM`Fs^H_85zmAk!?zD;h?3t3~JF9avUR9US*HqLEZ(_~QqSIuc3(sP(CPUqp@Ek`x32R0S#6&K!N}XB#1z*Q$k#j8f84 zIEU70P_`U-X@a2Rs7OIq7~)b%&|GVQNiwZU(bO{D*+FHI5SAj@arYc*A+4*Ta;lMa zo4jI@otnl}^?A(PZ;7h6TT&lFRN)cD*u?AJ1Z$4unqsygy>&TuVqBPuD|xDo7{p53 zYK^weGRnQ0&-Zhcvh`c)dn(RdVGc67L@8gGQk_~j60+y_5(=b&B5^WIL_{K?6o&P> zc4E72mwHLS0)_a8{<4HLw2omjnmX*MPBmHqR}zv_!Vd&= z=Mo}$98bfTps9CdUL3jS8QH|!;DUirHA03xn!;%&z>q*);A(n$+ViF)L1Q&2nTY~C z`u)ZTB^#uX|sNP_yh6*}6X25-~h_wSrXg$TOZ7aCH#ssJ1H9PRh#fY13J zUV;>RPQCz?lgbF6gnYbhX~5?EPj`nE97Gy2M;5(9-@K$NvCna_;wwPW&!5U;6(2 za`m|1XqqyYl$CfbAji+do_H1`BG?{OKe(OY)#o3f7Ui~oFUYnVD{+>4GW{wPu>c#k z2$>QjX{Ws69B%di@}qzUeK}Hf&x~LJ1xN@lexyfusz}{()aMZA+ZTIn>~73(Q*j!HSaGC)45`9} z1PT8D*B-d0+a@An0yuCH0;gl*C2aU$sVcSaSA4wTgWUE-m}mR2quM;&EsBiEOQ`J6 z`SJIz2$SI>$4q8+lm!`U$oeDJ++x#bl?PW!)92dop3QPJQ`Ww;jxG)^K<2Ji~2T>H@}NIoM17UGEg=;N$3dA%SHxZ+t_u1|Ep8tO%e^YQfPVS`D0&&)xD zSgS&-N>-+jgd2xICTBtSt?d|pq(}J;&UCH(p~XiL>kk&j=SroirYKU&YAH&CYbitn zs1p&^+W2Fc)J&R6)Mgn)HvnHBW|PK~!mdQ|8G;Wn@5N?3 zpaOYyq2+$oT!lhh6zfO}Kw8afRY1~`nzn)dmC*1J1m$ZdvFja zH5opfGKQzyS0LE^nf7kljKedfqh_^K?6a1;*)-K_`GIJ+O5&9XbwwmBopqcUrWuO< z^fl=i{{ZGVICwJ8xIijV3%NDTvh^`c{>;A4Rh!S-Y9&|GIC7M*)@Gunij%^oc-1C2 zKsOrdW@iH3AfhNx=u^DkoOw1L#f!CK8AP*?NdTu(uD=R}nrH7@@b3Qrj&emmyZTg8 z<_#)8CQ=(}pd(c&Q2>G>On^j~u@kVsko)DAVvAF8?Z3lk#5=-Crcl8bh*a0!E3bT> z%3JE({#!)u(q&6yXQ|eqQC8cb=2}Qy(h2}dNl)pWbh$X4E;<7&#Z{4cI8{A?89x&K zA&-O!mj@BP;6$YX3+t&C${0PX&uTKb_skd8&A!-ObNg+uF zR3dceg)o%VjH)_+ah#iF+O{G|-UJ(hI8_iN5obQw+y2frOa2DWw~L==StDpmZ#7v_ z#N*5)j?KOxIJ+?eyU$DNdH(vPv!ztl&Gm3U8#kuy{O?)CMKznyFigrg@GkrJxj|l!t=5 zFjF8~P)M|$FN&?NXqHSFhVTP)tMMo}y@Bf<#r#|mi6`2Ml@DrhznVqP?AA%MTXyvo zMFvrp<`0mSnwM$Xsc|b1L2*b*wOhRH<<(Q?l@G0^=r(<5OLVl&pal*VierRDzye@_GI1aLT*9*( zRM#~9`LtE|pN31fDVT(*DmguseeAI{_Tyk^_ieJ-We)AOcAkqZZlx6OjFOc!)d2<6 zr6?}8kh3-u(8OUKwU6CJP`{VAmXTM3;j-+CB?>YWpslFrQt-FkUeKMDu2mLr`&nC= z&`;&E+Q*SoQsqjB3tNo^6}F=y3X(2l+V}w51jR}@aO>K9X&mhsD6B}(5t@p0t`@(-<5Yv#!2)Pl=~>NwO9Jz`*Tzdt=|u`&0R4{piGw-+W1FheY^%$=&~)Sxhutj-*1 z&XhWCVa)zFu^WW&Z#Z zAH?3OmXkT$-s$tix>g;d&okOb;31>LhiU1aZAuCOSrvEIVv_>~L4r3_0Cz$JEnw>A-si`*t+N+w#hz?ZcT}Mz0%_BN)-wu1 z;G~!YSdsf*)0FzimuHv~LV|eLT7+9PQsYVr{nDDJ6H?~Z?FGW1LU_OG>1g@=#wKCl z%*4z-*G|Hq^RgsV&SsWinW-qOtKSHA_5!&{s-!~-47>T3Al!g^M8PTMCL~AC2a>$L zAQQ}HoSL9ov43tI|Y_gWliQ;16eC=zh1>661FQ+gm$15g8-0ZTENC6 zN%+h_Up4Jr>F3@w!*B|c+#HoWsP#i=qn~NK-}5`E+d6WRg6I4{VhElMsmT^~kNRvm zM=T2v{{Xt2f6{;XrSJNVd?*k0p3Xxj)^Gg`Ok_hLh>#68G@$H+_ zXXze$1g7?*f&j^#jMA z6etFB$`ljCM@X>rFitv}LWNL^_R%5%)n_zI{Ul$+Y4;=SVm#6i2@c1NEeZ=<&M6Xu z5yUx&7xgjwM!1YBcunYGL0g+eOgMlbSj3c@j$S_a<$%vo!`#D)h0HZ%mk zJZV8CEuQ>~PYN%rMTD3F_~-njSx2FciCM$2Av~A95bf;7`g0B5z1n5eExlZ)(z?Xd z_Z00amBqlMt_bd!GA*gJoMt>uR>VN6r1fuJX10yjgH9z9s+1t7Q{M-RC-y=1gsR)U zsm)Qcbgt2%Z8p|pz{&%3&&d42Wl04j1o2%GU|X&*J6;sZRchQ8&QZ}vAM%(7~1!jC-692FF*RYDl4d7|BLK`L8_kWv(@!h&YtU^o*Q zKG}szI@}%<9p*##8Ib<~sY~i>e$^r~Q05hzBbR15g>KN(QRP$)E?CW0g#KyKB`6AY zNeU}a2?P>go8nY7v>ZGAX%VY_8~CryvplkwI?ifxyqW57X=(D>mYZxPbjm_pjxjBT z5yYV)Cg9;m7^8YghkvZQQ9z#6`~BN2j+naRe3ANefbB zEh^$XHc8|SmH`SV!3gFv(lP!kvzcX>g}$1)dm+q6J)2=(_G6pXRk+b^mFHY>wmGy^ zwJf8_JW=q)t`x*FQM&Nq&x~R*aK`$ZOc`jTQ_(GEqBX(@91t#edI8Vri80)I+IW;F zm*4vFj4@zNK*EC*p$WCTXYc3l#3MN^LFSsgD9Dj`B4EYFxZy~eR0UKz6t0f2qhn2~ zd){|4KK<8>SrSc5NP`5HLU>iDEjeo*6Zfpb#g)KiBq`=Sp+bK&rfKA;Hi6D3Un}#t z)1Q_(np8}i$q%e3M!8O=T)IFWUtc_O3lt7zDa>{u#X{yIauyP#j(}gGkH1@DJRUx* zR}w5aTc2Os4k|dpf~L|`L4KXQ9G{0b_zV5+s#vG!yob&72LY7a^q|(}!5SkHMRF^_iCQ&T{KZ zEhIM!g9RbbN=k_4EfPH-;8>2F4!H}l?UE<#5|DIM`K2k{W4hdt<4LphbhN{TmYHBo z{+<>FgPcTL!%Rd3f}*j%@ZH&EJH%v)l(?t#u*YqcP=3A3jv%E;n-4pkE$`=m;&o%k zyrSy_OZPb~L_kAP;U7@)nx|XbL;FZltVj5Qu1BEq^caOWBzf`S`r#en%_@+g(=P87 zb-#SaFy21f)9md+J4z-hQNWcZ%OUjlI(M#<9H$ifMjXRh`eX=VT} zp02|G0H}-^y&lb~l7dylTX>}t7qp9b9#_TY45)yH_5qhc-mr~IQqa<7BsS~nKnYS;5M-q^PZE)CDC^A+?mrli967QGcberrDSPwy0-EVZwztlN!B~>BvD)E>zszKvL^g zy5yAD%%dXN25NIRt7(5U%W5K>)ujkY;hygq)PLo_Oj*k^1qg;NWZQ7Cz&qdoKh%u% zrg;de?{9teX8X%tQ?Xe{ZmNkny%L{IGmg4|+f{8a;8{RY60QPlsE8!m_~u#Ni~B|= z`3BV{+dky8D$uP;*;PG-qSF1I_BUmAGk2Rmm*lk6=&n{lPOAFVp|sS6yG|A<)bRk6 z0zeXAi6aIRi?Lq&>=VHJI$5PNAUQ2pjX75Khu3%eQWFs`Ixv(v5;){#9ggY`b{Tw7a?3jri#1yUC^MlTK!A`4>UrRj;u(lF)GDf?y#S9b{z3eA4q0Yk2;M>i z3YR@wL!4I!v$wI;e)KoFU9nqjR5E(3p0H9>v%(t>IK!bJaIOlG#-0^gH5Y@6J)0E- zpqrxTsn6gjc=BDd!z9C=aG8M2uVe+qB3b)adlmlx*@Yz@W06!-8ho{o(OU~b6ylOB z00vBvHWSV8@Udy2|gMx77E{|3;^3$5d zqkA%b9-HFY%%d<gdY&dP>yOzNfQ074Bo2@uW38uZ!um5E=|PrL zaO}4#r=X%M4G(~9Soo7~845WnDxki9HL@G++@Im^yX}wJMw{CTwC0)Hotw@rq=Yux zg)&)MwO9tm@o6~o-xTo~W(6i-61_u{Wcajt*MRJY3VBJDKp^vauR!rW_kFufir9>q zsvL`FsA(43cok5?C=MtA9A!`po_yf+##rqe#GzoWsl*OpPwl-1Nr=Og5{xt~5i{ec zgUrKc)kB)oW>o17rAg3QJU-xZ(*eZVSqW_zbI!#hnX$rtOW*ZgwS`+D8NO3hlsXip zER}_EaFml^LcuCRlc-R*i5d(9Z2^FoU6h|Ly12<;M;H)@e7<|980nv6`xUbLQ1)Gw zWm(41?L9cA%(Hy*x~&Z%V?w%zO4B8CdZLQ}0Pyre$W$Xs2=8*#%4N9U{ zD_@40Bq|#u0O}MwH3>?Al0h~giLta1G7RDgopFH&)pKO)`)GzGB+%6kkT||j60R;4 z6quV@EzoK2^~BtHhtefY1y-c5ow)-B0RSJUMP}|e8F^_pT8x>A>!;t2Gx*q=va)y1 zw&4Y@PN|a0hI+No%VYXZ>O}lE8iAixwy*lI*gfz2FAz6q-Dyh)Z z%P8in@j(1a2Fy`ro1@*XaOd@^wK+|gX1Q~vL59{8)3nXH+>m%wQldz)I$s{k;D5(v z#lpgyckwclsSlM(Qme$qOZg9ujhGo_m@puv`^g9#E6jCvZJ6fO)p?F(Q1wIgPB`M$ z>dT8#+)-MTr4uJoWS)5RuMhC!f@LOMDc7F@s+3sqUl;L=U8fMtCM9OfrmsyJ-aw0j zWGjMakc;%c-<~wGga85k`c^4s+VL?ZAoBr201;QFykJ#2mR67yNU@pm6V@l@aUxtc zED2>_Wr${Cemm`LeSY-7VM2PR zdCes%;aHBO-Y?YL04#hb3$Y3km7C8Uq2Yaf2p1m-(@ZqjfgWMhIpqf=22c*Y_W2kE zQM~RHN@|pndJ`f%Fd3&%P_h*Oj`APeF_-{IAyYqSCOgh`(4?Bx4w9lI_#R~60G6kL z=F!Ia$a(z1Gn>k&OQAoRT9d^Zl0ca7pA9F=0G^|Vxr8w|9FYG2aOv*ndD&)HUTAiL zsX2a8npC!P9NwL`s&h(8WPo^SP?e-1u&x)LT$KWnkO;9-Arl2=EP3$C9#XiHlFttQ z*yq_sd$dgD`3Bh2Z9}zLdq~UIM^2^}SlfUUB~k%ZT2mdoj-y;qZSpSgBCJx%DXl6l zNod&kb|MK9pu2$F4?sF;GrgPhoa1&Zv$c5(`G!`kvF1OlQLTrXrG4a(RV7IXP%uFg zp(hzVr)FYIr1Ebrgf2qjH+v2^`d};6p}t7m+0n#aPUQWkH+#G~HnPp8LCbS&#f22s zu-|J;>){81wKWAYrGN=dz%z<0Sp>5bDnVV`tU<6!qf#$6Rh&#_Pyi*yLp1X@cniKg zjoJO;?7eP6?LQJ%#u%%oYqCpgDNsY{3IQrDD2D=cJRlu#MH!BwI}{Y zGO{tNE|eOWt(@Aq6{_x}PkCpZ)K5thsqr|t!4m}gN(3dB&s5jmGFaFknT=4B-yIiF zu$))`#iBgMrvCsBmL^05IUa^KH!*Ey-wY^ZIT+pAkRlYN9|3vs>Gv26_&OROR?mPb z4u>Pqz}8St2`K}uy*_d4tz z_x?ZD1Be!779-hV_od8rfNhm6P@+hWcl*aUl9gvpu8{Bwx|ndONkH4l^b!X8khGPqs~0to|B2c9~E z$BH5i#)p{cg$g`j1;<+e6#}%(Ufe=Ypu?h;(NHQdRvP?635b!;Qkl0q^^Twa00`Z* z-Hz<`EqVUjXBlQj;He-~RXW;MrnLiwYDv?X0uDKzNsj6Qm0F-F!X|8ZyMUELP!4ED zs0;Cnx7shoZ`o60>D$TN+f-J%5K>o{HXkSr$^?+U00`0z@6h240Z^gcM;6fJEbT?U z*?dTe0Nw|lL^}I4L=C$9U;Uh0t5PzG%*!)UyG0Yilq$-S3N9_Jm8FIQi?fqJbtO0l z@0cPk_FoeOAHG!~l(}FD;=E|94Qs!Q4&$Fgh#%C@777#yo)AF>2v^I*UlEQ0Aq=Ez z-&PSOO}W4`Aqs|zok<6cI>Sik;s@;i0GVxGPn??i>UNE9_tvILh~gn=Ch|Mb2|8;w z#4K%;nMvIxt3$7CZV@J55C&2V!J4US=ubUjZX?^{+3!8c>1bQp`Zip7E%5$e{{V0* z00~eEFlIkgbRhH0?OO?%Qb~#u8BtXfilqje1WMVq=}G%%log?vIrpkkkc!Rs(Yx&V zJU;%gWf_^NH@@pBu6r&!XluhNklqk-c^qGxHd2I1(Od-I8ZxqS8P#(|n=9u7L%0(i9( z8rxB6_eLtQWgwPk7o+)hVq}vNg+jBhpAT1u2REp4hN;s~DyulBvTY9r(21BLPMTlb zI>eqq#4SAeiu8DxM(GGtqSO1dA-0~Os@$o@Qd?%B)|QUWq0ZE$3qeYNGC(j)cw^kC4@iJBjk39rT z9(P+kK|H{%DbaX4!(w>uHWImDs-yq~RoOlwtaCqY9ktCW>*=X!t6E!%2`PG|bq%(K zD5XkC3LmECMvkJX>{r<0c`^XMJOgogD^SR^us}= z@fl{6?=amUbEgG#Se2jgUR|(xPGhutU-`=Xz0A6zqNb2AdbN>rqnNdi-I z5Dp|@%2YMY!sY9mMmxsA#lx5cD!08)nYGqAAKU)`Xg|vPPwxYHI|)kuTb8G2Dypby z)`wXNqgvGfp-60w6@|EDcT`Nl#gwv5DiJKm2sAude6`faj`*g^MDr8&fJ>W@p-ZLH zUqSTS-QM4{&e>CCtutSldEwv~pa!M4EIUPBZY)9L6iKLXO%f+mhqamU4 zYTONo;>^wh1(h>(=WeSOpSxefn`$@2?p)T>DZP}-n5U*%7N*pPl*!>r)PPcxVIoDR zTjMFU?WkrVU{c$`n<`)SQLnH(3}SAAi!{=)0_pp0b&FEmm*MBQ4a+}H^&Udb(rp6; zvb^IoZ4M6D4kEOmjbuol0Bww}(c*K8Yt>xdg)jpA<11k)7+BCj);aBV=bsMx$S z2Vs&~1{;tA1#6+>t#oKnQqwx>dp^$mTyYXi&YonA1~MB~*@=i$0&R41gUdYQMY3Sa zgoOpjWC>bu%;(KRNaAV28*uOds64dP9WF$V185c)9u_3%;m9qjAkK!YwI!A;rknss zD%BYuew7e!n~%>?B{bJt2}zJSA1@6(4!C?p;vcT2+<3$Z@n=F1-41;KuR8Mpc32vE zr$tBzDngU}$o~K#!U>p*kO#tWISj+%IXo#cBg zc9SvAK%iSnSVJjq^nY-V{n-b+WAx6a6D6FB7QJ58=Mr%7CS?VI(ber$_e#WX%=Tk= z%+@ziv~OmW)T?nT%XVL9!Kcfp5>z-^s;N)+98D=RE3x3grNo(!==o>PI+|_Z;4nNj z{-s_qOGd~e`olDNhl;ON+9>w(>)UzmSVxBSSv|U?;0I&e!5Y_>j=-E>)Ut_jmP;Rc$3o9i*+UrE|Yd zr3E%nf`Up)NjyZsAY8^hEBNQ}=_L}*WGbN1`7(uM;iO^z0FZd-*o?wcx`48Qozt4V z?5oR0(QO`cwOP$>cb(R~S(?^9sJkCapeRu^@oE+uS0wz+8>t2wj$*4P5 zTd25GF1GQ75oXjErMZLdoO3V$vE`7rcZl(RJDJ`q8IQ7lce9lA4OG>uD_XeRS9m1Y zsWK$MlWz#ea!|GF?aSUc#AznreA*}@+K!b>>_*)crW7i&h_DemNtBP@?^yVw{oLlk zKkk>#1>sBn)7g*z0Ke<>F?k5P1Geud1H`W&qc=Y-Jx)DhY0**2y8i%i=I{;eu&P*4 z97aJJnZ3Mv`HvgOrksFDC#glRK?;yc=6P1>GNaT=&yBwI^}v6`&Dj0Y z@xw|`O{xx{$9P|yr-F80qw-i@Y+lnlvztw`*~(^ex`3e~-Q|gq;v#zdF;}%<%{0p^ zi-mQuUz^m%uVTa!$yy5(O*O3_$}P8N{j{3d?UPPZnB*A)^wo_i=Mz;*m5PS=4;zbF zOeMgX;gSy&VwY{h-N69?=D)qr$eVNv4ug%D|Tya&EQZG(Cs6(X+eN zT|Rl2Y~>9$N0sL;vsClQ3@V)~QAZs=Y7xfRn=J&S#<#{37E!-Ax}5mFPoAPZ17zQu z4K8IM(bcr$`=!Z0vKemCQa4kTW{uQP09V;lJcl4el&ChP5Mn_D19NN)l&e;s*eEW~ zYeboF5Gy*UQ(GoY*hr(F#Fmd}_jQrKU9gU?A*jrA0Z&y|NldjUwzkV|xaSHLRcr$Au!Qp$)|Ha6g)uBJH=-*!i}6`7vUW*PNkwbbjVc}F&J1vL&Y zh@KV53IviQ9(a|3{{V;!uctV~_?{^^gqHw-9<^t7u2ByaDI^6eg(rj=)DC8JnCm>e z^%8%z6Tw08gwV5B(_Bd9+wQSpon;vp9EUTEG>#)X$d1DdYx$sD84>_<4i zxu%?>u)1-trf?G>H6CN;0#sH5Vn`>;%dV3X8hTh}acumv`16D?&PmVr+MLSoY2@%5 z{{V~&p7-6xIs_U);7vM%$Agxh{{TtX-OC-BzR7|A07*`aDd!3R=9y6mD@p)Tu1Olm z@%8lhz?MfjDbagJsr3>eB=dilNe0KDUU^^F z@8O5Vl>tB$C_zHUyx~hh!@u6JX3CP#JUtIPc~38tVgZ=%04Sk#SngW#KAB&K#ACNF2Pd z{{T#yrEdKDNHE*CLaGoORDLMB>S~Z?%}aqWT;d{TQ>0$j@YngrM+Xf8b3jL#{YO9T ztWWQ!dOQt9Tm9Y_ObrZ0`56E~(?NjEvq>8iNl{2Ywo~6*F zoA9ke?ngu9dPN=^B*K8Nrer5Nrh1nP$1!9o3$A5yNAmNuD`_{IwRv$trp_x^SP(lp zr^FGV9qF;@sl;q=1d#DLBfZr+3LOkt;r{?7#gvC&lq7Z3a8-1)IlCk6VZAMdtwm){ z(9~AYg`!$^rPaB^X)>i=z>|5KdBLB+@R33bBRzQYw@9Cd{HAttiUD;}Q_U<}&MZ4} z{@xoiv^BJp`#|R%O-h?-!2ARPr3Uzty<|*@H}Sw< zK)y*(bs8f2-t2o^xTGO+L#}u8^3oK0na{ta)}ot?!jEQe1zd=|!PgX+_+*k&&!@9i zH5e0s03>C|qCGv~`r7)C;v0MN^+gICp5c&mAj|>d;fOnvW#%H=1Tv^*L94%Y^K9y# zZ7jN5cAlMF3WHft7NnRWMfDnuIcbNMQV1k4DqMc!oLN+^X!=m`O2vBnQqfX5vN?z6 zYLfWus+Yu(-B)pBLWR4~Ak1ET?DkXjF@Qn?Q}snisoSLz;A7FSFlB#hjz}tEkbGXiu$HosU4``edHlC1OSk-1RY0&nYl7Bb~7uGE)kb2_hxuQX_8P3 zlObZ<9^riQNO-q7zdf+oZJ^7L<#k)l3bs`6LykD2$#n(Z5Rgec5=3}-;$+wYPyqn8 zL+3$7OGfYb-;H9*1cG9UCElnOEB!S|b&iKNFKsp}dz%-RWIIK)j%Ar^?6l$SssM3H zQWCgZOK~a@p$bTX05g14?J}V(2kPx}MB?N}s`z%%HYyp3DxQ5o?P6W~KkoA+$PrdH zBptGKv&Tp~iEWmV-YZE-DU_rI13HWI#dhi5p+9v~UY{|l!^7UgQ%*o9eEFDrcFQf= z`u&O9H*yL(N{sa7wMj-W6nMh;r-Ph%&)P_s+JGL<&Q|fc4`mI`ZYw6s*x9X{ zpk>zG;8tL|X56WHK_rza)d?k58k}OhU->Nfvdt)=$evWpFCoe#_;2y7JUIz06GEAT z&M!&PWMzP3?DcsEgg7 z!Z78YQ;`II`J*vkN!P9HJ{-O{t;MJQQ{SX1@KCN!b-S~Q`N50z4ZgunUkz0$K(b;b0F$Za zK=JU?5fB=Hc}Flwnko8aqm4wV^e2D=F#1R<6ES~Jo*on_atzJ7reQ)HV1(47(qr_< z*2X@#P@y@sHT9WgQ`IF3Qza=pDJJ4&Hq^oSiNb{oU7yV=bF7`9+6Z+iQj^*eJLDc2 zHjBi`5ISSIWmP~)_Qr)u_@ zx&%271f&IAss*pCjSjPl>7|zt?#K@DBb9CXda)@7lM-Ne=222Is;{xWanI-CdH8^3 z+o8$oHY;XxM(VboG+}6Q6mJI?NJuFhU6Pcf86rW00$^iv{BOW9_Kn&14((TWT|jkC z2RAXD@$Ve^7;_0GQzWx)iaVCiR1eZQDB6v!&i21(GhLz2D_*Y5vx>d^2A4G)#LTZ3hiBXg59uMQc$Tp3y5Q&LpK(w-fcH-d;Sv}OB`H>{+ z+?zC(-`^S%SBeOS+vzdj0dn-Pv*x%owNsSo0tJ z&KQ6JAR$8hp!js3@GW6R-lyX0X}mv^cZvOJsKKn zo+s81@&QycCfZ2^Da0qTHc1Ll1t!`k7@vliXcW>QQP1V$qV3!6r)OwAjvm@)>Y2)O zs;rqyWwxA1JSl9evm!wSQ2-4=j;96%B?lqt^8uVJvkfDhpK3bK%#cUi-BmU`Qj|*=T+3ojZYQOuwE&TF}XhW0!AbaoY=Z{q)LvRT-uj`-g zJkkUh;eqFeeDf=Az$M@0NT`>Zdgub1u-q3`3= zM~kIQM-SgEG?Q!VhMH0l0ZkppT*P&=5xzHwK%w$a?^y8Dp&%5bCKL_t&d0+^@cE;K zy(u+2szDE~ypXR08XOtGlzq&ilxuRM`K}>lP+~!G{7aLEU567RyY?YKsZl& zNCVPwo$(oEmkwZpg);})MwJ}nQ{gz@bfYrB}tRl1GW?Dj+($#Tc* z)WaEj)HUdI*HGpFHqAvsT5#~K3Ro!$AeD{~XC8Zs47jt@{z}Xkwz)e|qtW&)u2CSA zu}j%m^J4es7W?dZmv2{NmZWWC{{WiV?BUf^<>habxl>HEf~S%erIduCRl#Y3IpQ4k z9QbkeJUZISchN8X#E6iR%xTAFuFB70DU1QkEo zfq5F^ydp^eW&lCOX{T!tcB~*o(;@_ZRXMWzaQydut9Mbn?c44C*ldR1S1Tw^S)5iS zbd?GmbrP}=J0&Z0ey@p7}J6-pr2FYK3Zvq6BZ=0T}FC*`81!VqJjSaT|d0v$4?c)hm4N8^6T+7 z@gSm{l-VPLh5#rAi8t;tL;NU-1Ocf!#qB?RG0-SfcccWhTZ|gzj&_SmiGWGaOijOQ z`o<0U4kOwbL7@f|bxH~;MDapEo}M0OuDBVH*sqimeB?a-VIi31%286nU?`K}9$jr1 zUz!aKc?z;drlVaJF{eZM=CQNL>o5O9Cv0Q z1JW!^`>^6tNMliNm`XDpL?|fGS{e^NT=kBJ_ZXQhySkX>X}}7isaQA*;0d>myrlg# z(+82ICy$`x=wLHU5S0p6_m5LlRUnlKwdeT2_l{?*M4Ea4U%F=k5~)dnC#;+L%v+fK z?CUWowQPr!4C2ft0aehj@+s{fB}+*XqMZl>pSv9WIo0O?+JSij{NUTNfK2j*vW5NpacA9^A)kK~feTqnN>WwDI$n8no>TF}xiEyF9p*$1 zAM{YAXu^=+Z~A}ve_h@QQu~G2g+#%x0-sGmfT9xv@AX;`qWm(B9} z4ArMlM*(%U1p@$;#A^{Yj;F&I>}{ONCMLmo1<=-!93;t8ab%0$cq8YLVnfmDex zpJsxyBTI-<46v9yc8Ivt^5`w&IHN}(El(dPxWL7rV2ApDE(@uKHm0JWHtLm0M@Y#E z{{X(#R2r1IYr`QWR~iG?4=kjR5Sm)iB79KALifX^q7eIC?JY8QU9<{9m~x_)fo-$c zFj^%`O3(^YKqLZn0wVaf*f0;)l%pAeCD8^}x$2J*A* zTS6Dd-#`Hb2q4;SaaFYJ>Iwy3!ewO?ZVTzY9paK4>s$?#T0*qJpmveqsK&ly8 zy+63IS}$*{-Igk8GR7ZK}-n*$rlBE38FHONWU^^teL!dO~FPZDHk%CymQ6 zEM`DxEmU&)iX~#ieJn`{1QZo+H_0^AM2DiLsBz|)t8HJBX+dQM4Z)EE&`tF6#z!7g zFj>UNnKKlptuxjZ9w3(t{lFS^%v>REI>VnuRg}}H2EtHW;Z1~0C*5=Y`5Llt_t=6( zRI-I^kD2KP*=Cr|q_D3f^!9r~f!x5hQifB+r8X1-Zg@nU1fSm$=9_zlO1$FULI5g2 zc*VTIwI*IL0X90=iyQc2J}C|r_e(EMzY)eH!B~oX^MkhYQs|~bNG&|dL*S3|Z6pMP z76v(dF%~06+{&fofa~4v&<81mOf1~#H$S$gMG7@+^+i%MC#u$t&6<=3Uv4atnBj5{ z{YPKh3KTE1CR(niE>t#!mK&7_kfemlNgTiNvwoO$dBjbKMhp}wa{{Xy}S%4q{K$`65hr0xhId=DXn^V0# ztIW2qYV%5({NB2$*X8#dQ*EKg6ni9sr~xx>R>!vg03O+J_E{yv`c(*^REwS!YnbtE zxA`t6)rmZ_5@jTX3R1UGXC>X?ID$`j{;Nsn*UMj9V_z(?ODYLo$B_QJNXhLO^5bF> z0dwvfl5^Gt$kuJw9pp&4pr{FxX5s;iKONG_HLbk3tP?yBZK1gl<9r=89Y=jqJ42Ud zkYu0G1RX`-#*y>FGO>ASo}K$aTf^CZ|!=f?$^ci z>~Tq0`-RpYX|z+am(&)7@K``FNE!eRo-u-t#WY9HEZUG9Nj}n@>7Vv0@k1WbbPuC- z`)gME972}7k$>C@1o7=oOq+25{{UAn-vGq_0E0gnf9{q8;b;E<&X^j_kL7`j-O`%H zyCfzP{{X9>rTJ-oT64BNCOp6DY3KTl(2*&SCzOM)sltc)i&d+-^Q3QW2~2YyVh=Ci z_;kl32$uxtb?chb`+ezi^?^tN27q`Fe*F1q;ev?`+bhRRX&w;Vp7FWpnGw?C<$}VE zh$#ect_+I{dRXB?f!L7>K9Dsrg$M;8X5Ci{Y{tdVZS7K0tggR=rkpGLsY2pZq?^D5$@lcc zY4O09b!y0J_RUS7;w&~y4X+Yu0G4ASx%-cjUDaQNIXj}<#@6jt?skuBY3R`X4ouxc zVpA$@2x(NP5H#@ILr97%Ph%eV@fEpWZH7{{{RQsP8JwfSyZlI1DPvg9=gP7 ztKSCFWvHjw)~KI;qa_JwR5s$fX3<4br)f`R#lr|$N}(W|7``$1m|N3KTBX#jd5-NC z^6dgbO%p$c)n7Y2Tko--;#;-*E17K8$M0F(w?4@#T&AJR-*Ba>{Jl=C49kjnNl`*d z!B9Mh3||m2_GAQwHUm7fp{w77UF<#}CqPJJM&af-M@gHz-cHQzO*@r)C74m>iH09} zrVyK8vdL0w--4qY_Dj{v`_`qFtPGSf`K08}PV) z+QxzH;cni6oUd(|dFqOH-RqAo}8IDX&BwR z1j^{ue>KecIOW6t0JL7@cJFc<%Gynb+ibR`nxkjyKP;-jO6n<`CjIBQJ1LQ&RL{oe z0}@@*6#g}~L0z(1=om(C8d-^$SXG*YFSn+QGfE3lOJpecUT?||obvID;F4kmT?&Kd z`h2uPGR!IudB*4Gg68pf>EYu%?J?<&aYpc%mSUM&z4=E5eb!R_4{7I;!5hk1N`jQ4 zo+3rZGt=*$D(0!;ePQ4$iA;-f`{#eN%;BNMM-l57;X&gSk_Vl3Wy?Kj-Jtq}5yDa$^|UDs`Z5h zGDC_5X3?*}AHPfx;iHfJ%lj9GxyCtqJd3 z@;YBnAD>(_Fd)#vI<=C}?9_<3Y=lsvZ;f_R$Fe_52&o}gi8^IuO%A=(H{o4}M=6b@9vB?Su%n{ZUctBRS z^Pj$0hHy%sv~vY{v`CM*c*snEPM7)kVSN%T2B=4xG%7=Zusc~(u^H7KWXjxeXnCTh zP_h!VrD9A2X^Zk0uG;VpETjdcYg>DI=M#2qq~Xsh&=p4u&3OL+RJV+i+KXaF%;k02 zonjcTLg|BzrG4ZSwk0kJzX1eKTL_zE_MZxxa!%ESM{!X~aE;G~`0TiH2@L3< z1H|!E4*9nTf3ja|P2T1?ODI-$f|Q^YHmZcBN{v8DmCwWXnxo-3s)J^lwGtjB2x{F0 zT+qerzAuDpvoLQ|z8xE7Yze1wRXHM1ZK0Ef&yt8yp`C z!SN@U1G5=b*LGl;1>;hDqnZ3-KnWX2I`2zWEP%4XZw4Oy>VI{U_dZ}Gv2EWk_%_8C#lcy%!bsiZB? z^R+5jNsFa2IUbQC{TyiUwhyKNL_#@}kn8A#MoSxN-E(jWfv*lqU$ zN>qZCaG)=cCrO=jz-Pm1CZdOna(!Y>4%RTn@F+z%i&wTx;0<2?0Cu*aXSC0Af|cD# zmpErz-%0o1OhTJvGZbX7A$TEGcChCXf7{sNhzAp&bmIlbeVat;xK)`}s`e`F4fN)1 zt~i@uBrpQM{z0?{1;SjbQ53_&yPs){^JA-1RZvZHN(tg=V~D9TIFgW{ zZbYOVd@&0X!%WhVyP2mR`3!_xY(L7jiD#F5kO`?L%;ncuxvyrA#IIqH)?biS<;cnE zirH;SsT?6nTp&V~!kD>}C*6!Mi})5K;}U6QCSWEsS4UjB)hQdTgZX9>ODy=6VhKd! ztN#FZxP6%0>V2wilQqfnDq8vs!KSL3T{S~*6bDi}M%URbTqO%UAu59xf&}BucFl`w z!NZ6?BhOsH`M#l4d(A z01f(Hef|VEq=XDj7=uH`?Z?i}LgZS$kTv?64mz^shik-(z`2-?ysd>;6vAbetXw?_ z%P*KbqRi;Z0CXj{SZyjvAWGT-!0F5R!iRyCIjavT4Jjc(qzCDhs3lkI&nOo2)2ACU*iyKVgLWK@hv<*14Hu_4!8|>;5E|MUD;jhQX3KSqS z{F0+9&l%1#$5Bqht1r9-mBL6>&bH=0M@J=cqBJPiv^znT?Jiuru}eW)i79cz{lu42 z04Id?^3$dVG`rVAoUg;k!SIIi3M}5j81^##jmYXB2{hFe^u42%0ZVC5wwN&D03}Tz zYu*B80KOX!4td7ll$AluRjO&_#x7--3j++zF?0b`Wa0gIS}QNx-`m4x_Y1Jq*?k3L zYT8PMpKC2;mzz?`oM~xoOG;E$lf+VFfnZKH-U0l77GN=EmV^*XtE~|@>!4WkujD_- zq}hJ1?Cu1}0Tu;02fFCd#?JLOskNKp+N{fIwxcqr%qy$v9ep9^mbU4TwUuxn5@IBX z@UamX^`8guiLoY>kR?W;!HP*3T{X$NJPrX$lVY$?WLi>q23eT zuqr>|e08@zetX@YW*aP^GKLLBL^QpR7|RTNNk|6h7emX1i)Mlh843pTr|&lNj7W)q zs0T2xIDU^seQ=@R8AQ9`{?FFfaZ%B=gUv(0`ay$9;zED*9~slmMxk~!&l_~=|BhZPy;T6{-alV#>U zV&*qlysF)&_IJ!SH#VaD-5y*!N*dPqf?H9J3zYD{P!csdMBwn#NKmM5^@@G=rOm49 zw|%#Cj;*ydO;warq^3LJ8&Q)eL}~#*Hk%JTN5_;5DbxO6`-jX!@cB14Rwd^1RQkJH^5VU;rtaQ4vp1MR=7Y z$#R4*diD4X2ch>I6X@&b`h~wXena9iKp|-`b?JsUC<$6Q;5i6FVn|ZTG=mhaFrP1X zpU%^OL8IA9l$j(!*VK98Lh}9bXk28OhE%0BO`50jNoi?usHx?QPA54?r~g1Ad-BaA16Dgh+UGd>}-sUXZc^}DAI0V%7O>B)mJ zl**`~5cvyNF_55&4%%Ppjb`mv(!N>#r23ZaNs^|~AgqI9Wp{wLEkxpqS=^9RU$pen zD5u7lVh}{Y63U@C*4GrT2M>2!)z}T0+6t8IE?TVhZL-2zs&yxcr`sT{_MIXdRiqmc z5(W{Qe#9~%?IO!#n1ZsLmiMaatNM4DZz)XhErd>>EFmZ|@H4jZ#aG|DgXIsAZfL775nq~V*R{m37 zjK#`^)A<*hZlMWtjuNG|l&!ZCI5>eQDK{~E9$B|a_sSz}IAahQSmZbomAAxHui}C1 zF`D~MY|Gi^@3vzvtj)7mRJzaU^bDcehg7grQv0o-f*Wk{D9nJ95HV$ih?0k~tr%*; z=|I}OT>c(Nnr+fpkjks4s)gemm3vcr2kp1BE@hNZX4Uk0j$xWL+Bru+w5{3`5>V<9 z#I%q^9w05QxZvZEe%@)@*U6G7vG6xlP$GlN*DQ(1#0RZnwH4J=Z=|;EAyOVm01H^m z#7D>7iFjD1X-Hbjdm44VXy8YGSwo!*#|kqY zU>fxvUsH|<2v7(m4_k~yArkm6hLtT5{W|m z71{)|Qs7YrRjtP_9=xp{I3^_fz@aLHttcDkrRf)XV{_^02XyLFs3PRXr`@Ugd~b*o zNJ(1Xr(>Q%i+JXG#znYGg~Y%q95OULe{Fr!g&>$4fpvxif}#?Y)J?vnDpO)fpAQ3h zkB%kFK-M4J7)%IN14V)WfQ5no07{RSA?|@}JUWcWQ33+3!ywO(t+B}=3yK@V5)ind zxPf4Hd@D`3fIR-kK3EE+(gO#SkDeHlFhowBImw^m(}M}Y4p80{3F0J3BoYjL^qe}3 z$59ct1$S9%-=!loh1*}}%g@6P2umu0ml}Scb7)eBm#?RfkDokrB~pb)D3GHu-}HJw zpvWzjbO$Q!wMYKW$_O} z6l#X?nL@`Xw>TeNFj0jmxI6%ZC>0=cjA21SK$zCj4=+xG-)&goNQQ-t0HSvOP?Tmm zh&ZNX${r{Lq-)R(HG|{G973A}EWiaTmA!Qb`+^B&yu*=S`>HW#g(fVw#hq1E=oLZgg)*W%5mwjdRVph}j&L5N&zwZ{Jn(5Hmv1MC$?JT_dsnX-9G$4#Hn^^NDC|Z zXOcmG#Yh+J9ZLGIdPk+kNE|IJ9ZI-TC!f2h`0I|&%Z7!K0ZZBt+OWw3bXT`t?P$rm zrH2p_$l;JdBgWBhhgrlig()*L22i#0?IXYOB7#x?Uhe5q)jVNT&S?606w3bqEb<3+ z@JLb>{v-fL=^zm{J~)qwuwh~@^s>wjh)*6JkVs1^QzLEorPpEn zf5mZd@nkdN8jI>0z-DT}G>)6Sp1rpBQNK!BBvR(IIbL&6(+xG3W;HD^;_F}~Yi*|- zO4G!m0(-i|Nyn4;&+%=|>ExB=Qa}z8tW{5)gnM^{e;~sca$$*qGm$A%n_o9{uBt@( zZ!>Xp>3EQPB&DYygThq-Dlkj~s3s=FV+oJ4PlP50Nu>d55xcl-<86gFnv)5U2mp#! z-we<_>PA+>Dse$BkQWy30wj~q-OOSP#L`#07vWd4lm|xMms^TQa#|CpwdyDAl{a>hn#MmGFBK%$W(uVhjltgH_-Ew7)60f zp+!9Id*KMl{{SzSf2aoja#ra>rd#hbPI)2r{{UBn+e6mlJ(LH|=9h?Dnm-&vPv0imcx?O;s*kmnGn;haE~*5aCEsS7a9) zA|QASZ9k9rFvrJ`84l{)YBRoG?HPX`{y=ar=9dm|I2xV`)NTYC$16MJ-v-%l!)vom zsoKoSq4iYMKDtU(ytN{o3t26tC{chClNR?*J)36moIRTl3Rz?yZ~&lNfM|#X-<8je zc%O)3?U;+heQ`L>F)D~S`LW5GI4M5s8xqBaZ=FXA zQUQ=8{14L#6bC7lO`;9D#Qy*uv4_ww=|00_Or;{%eEJ5T2S{VXy`&mj{Py2fe35`gCoM@UY|`h=ZP3{9qD+|s?g&J2rd92RD1rQ(J4#= zAAiBcluC@1q4Ga0C{d#k;YWu<(0?QH#59!&X0AQYm-s! z8nTLt46s&ZL2sy3Jo7}URLvBth$&nkaV9`Sj8N_3SI)zLRgJHN%tWG4N+h6a);jS3 zn1SBae$#ckdNxZgt*&{qvw3Ky%#dADyLync@e#ysqjMUadE?JKK!3b6{G|%a5cbip z!C3*`CiQAuzbE4aTQi)|?RLShGb4=BHcB<)geQeZ$cXwJ4q6To2Wy>XE|hQ-S?QJd zhpaODIh*X^%`4Z2-oX}+&Pawh>;r9${<`$w^pCX!-)tWvk{-f zDETd4^xIR<*ICyMJln@k2)>Y@Y8#LW$qA8f3BTNZan!g(%1=RhDsj=cLV~v8DupXsd3;Wh{;>RUBxG?(m@1 zr6Enn7Zgbot+eO&Yzq=vfp_(8w}kLjh8l}`I zCxD(n-<^j!^W_)wTcjF-4O9ip)Mv)oLm5UFb`s&FKP#Pc)t_ut=%I9Ub?IW6J0 zWynFZSv%v0iB}h}BdI+{rlS)jnF`j&-<&q!%=vskg;5j(CyE7$wa1pcwC9OZ&JZ(a zo>?Mmb z`^;mF@DUyEz$#`SbIL&)5Dzp?^@8Qs6sV`OPcCvj=lSr*RbN27%1Tgv( zp7K&47!fzW-!5=4pkP_gnT%JMJO2PJ2!v=A=i{t>$i6w>mp@;KPzI%MG@jK7QC_{$ zNBiaB>4m>8en;XI6;kHBPlVKWSI45KtoVGex8={s{6d8#O>DCXm?;3P2`TpvcfR;r z(wq4oh)`coNX1DidZk0u+uzK6oOZlA2z}|ZF_W}3NLO;DYKv+}*I1o={ydWK=s0EF zYYTd8&sYTJT|!(6TZ&b>pC3M2#rf-j?hMag)N+F%&_?%Za`4(x&?+EKrqFrLg4jYp z2rk?{At1{tvh zt2rn^b~z>8J>C}R79IZpU94iP%PnjGf|Z_fn2U9Qei-9$W_-RQt?9Ed>sf*-76VPE zh6bW1&(BYu5s&~%mKs9dn=_w(6pU&ra}E`R!$?Ah3Q)M>0E@-B>CSz|w`;;UfP|P=r-;DV9*P*&VCeZIsTk?W4%ED!MA_bvEKwhm;hRw6wT|12PCw zK^`&J87;TMCdCtxDiS6lg;=e%_|7()2aSv=u*sbavjh214D6NU_E_ng@c8!ZcY26U z#qSS0re{5)rl+K8rn;dBIh&v*2nyp`N}WxI5dQ!oAjtzBTl|Cg{PN*qP9#9;&T6f| zl(nq>#r%KASZOYzp=-62D&L!P0Q#}4C{{Rr3R?{U-RySB9v2=wA7i|83I+1NOI1Nb_ zLFP}BQt@+zB(+Ne*S>dyDs1hxXdbLW!e6FujHsDcVn=iiWP#A%Lx7TApk%BxpBKNwGEFFi); z9+ZyR05()^mxa_xybwUV&8;|6;s6w`?T%lsXo)nW=AwgENYh2O?G_$xzNxi3&@Gge zr--Bou$eySn{u2V2Mlc>BDp~QRh&^k;Y#wRSr-#7wySfB>-M(xtk~VhZ0>KEGgQ-6 z&ZX`Ksoh9@*NbWi93fmvl~}~Z&99BmfOsmIRAgK$lS;j->lr^5@r|Enml93z3K|=Q zN-O5V@0TKb_uo#~Z?kQ)J*UrW-*TP$hnJVqDbU1^2v<@OQJ{rcKJ$x^P z9PR@!I*fqy>mOAmPNG;%%xEXqL-FM}Q7S!Zj;$?mDs;So;(j`Nu;GX~5e+pD1L*-& zRmM1z%Tgi@{{U@1mchXW?5RWBKt!bXR!}+mLDdB^UjVFLMx)CA0PFpBCP0RSbYC

soXV0hz*92mEizF?(8r+~bkanyL}g$*?i1L+3TSq)Rp zB}^c^t|b2eZ2+AO?2u1~J$(v*fo0BN^&7$2 zMjqlRx=L7i)m#aP9wOR5;Lra6m;UKs-X9>B#5QxTFBieY{fxANw)>_+Io?u7&~?YH ziDb+b%#QCKO}i)hyu=6!5+o!^9HXBsND=^!SuWm`gk+cfNyR@k=nI{w&kh6w>nl()nn2q2+nr_6mzPMVsAX~Jv!i9Oq-yl zM2pd%tN!9%AKf@o3!yx~bkZy{asJ5rWu4H!om3?bWDjK(g}0f96=W?*3JH>spa#0> z=a2CWVkS*U4qrMo?mVM=;TTd0N8K%y5)=-ipViDzr?o~>t(o3d(&U^90+TgbM5Y1Z zTH7F7mkdSX!x)XdOyuTeIwBsZ%Nj}l02b^+iR|~Z3{>Th?9D^Xj^3ArC{nG&dw>c5 z04^de$P5l%N8)H1c)&C2ZBYBQQF?>&3^enOX>4%M?(8mK}k+)7mmrbv-EQJ}+| z5EP1l+?yrP#k*`bwa)KRwL3quyB}8N4IV+1p@(xPTU&`+%tAN|C`n7fG@`8GeNO&jl_3b? zOu}3#3MTf%h_k~iUiszUcavLXwza*T3hQ%xvZG>Z*?G2;-7KN!TT&Yg6&?kri9+cl zbC?)WD;ium_IZrkvdjTMSgkZwMf2mxN8&@U8#3g$o3b#TCvJV z)a&ZL^np!fwaP@EBR(hV=psINTayxrUi;^iB$NJLusWA0st1W$*v_6kK0ZA#zJbVf zJ+GL2iX@Xxr{_-8L7S9rgz%GopkMg^09U>Gmjr?o009*T%|s-SJ2tED0V*0&p7@%;w~(X z;d3CGqj&m+2hD8YQA(U72$g&8O{3sU;6gyC1EHzwTf(d=DKhFxbfh5yMfj~Y_kTR6 zpA$-sIgfbja%{ZE%o>`dHj&xDiM(|2>C4y7RF`n*kH!|{*?EZ2b0vXtz9TO3&ZY(A8te-L*G~@e!VK-jWFiq*RJ^J&3#>CboXa!QVj<<{Y8Sul2i#hMt ztSBPPm`Gpd8yx4#4A0zok%K7DzQ~0J*#>P-QWniY1foGCN0|Nx#}6$>5$g&JtBQCE zX`12W6~b>CO}s~3P2HC$=Xhj#S5eVjAfldvxzeSr^rRc>=VA%>>*I&rmm$&?@Nk7i zk^AD2wB*%}p6zk~8UTM|B>H$^Zp)B<`ErH4HR+=Idd8ubJkq!vs9M#ei2$Dqe1;o# zP!%Y>>&_PNW-gYZ=LSq=#RPVeQ^XPu9~&kyw^{{Tnyw|6@7qeH0Iq48C_8_Ep?~&4i>|0t#-%l9#gb=6;sH}GP@)$)zQq!aR$m1R6N$!Z5uqH=O3*b}X zqLf;`sgT#Va?OYx9NfnGjj5!{)7l__SOjaN0}^%A4_Uq;PbP{gnQ*)5Z6T(VOySB< zBfeF?q$4(iY^pPSk=mMfLSCnH`)T5WpnPV-p&v|lg@H8bD3qP#lprjsGPib!n4(-T zvoJ+_S3m4tAh!PieWtcobGtK{XZhW78O#}`eYPo(_xByB;7|kgaS~SmkdxvDHogbo zTf$O{qAC!HWwv|@*A?UbJ1!0!(&5c0Vj8OG!OiIPW{z9;`Rxt0+w$7V%-=n$eK}dE zclvE0bzPbnQ;OkI(o7W;nbXq49Fu1EgAK4u>Pqz(7(Iryj&_U@~KgT6uiGet!gqBhbMTPx*SnwukcNpe#>_ z>*zd%xt_Ra?hK1*Ab^`+#wYLR5rqm9HPot0k^qh(PLm(ESg`4b1q$|Ik&re?+xx+_ z7y*P70Wk+l0&Y)_*GwM3+}U&=zK{$+KIM9z1=GCR2ejc&5gevGV$<~IHNuGSX-QV# zS9~53*N`86?+C(PLe#Yh zy}+?$EFRi-+q`!vAk`FwhW`NZaoJitui2w}wX}{iP=t+wbuJ}oT9AkLnXxUlL=r$K zUkr^wH^BCf_%r_i<^KS>Sf8+!+xBoQaaaEU?Bf2p@{bE`P&X8AcD+Be-MR7 zey!F7@TaH5{`e$Fj|x%&wrNKZrl3a2fYsQm3skVvjtKL4*=1vYL4rc4F z5vf|-QrsuaYMJNm#w%_Cz|Fadv|b=R?HbG@{^t{j9%IhQ!H~KGbD6`rZrRZ#OChHS zQS##wIdap-!w_(2I3HN3#0xrR-exA2+R!nXvc+{ec+3T;n}{S^_@4vM2ZQ0^?lwUB zIqBWYQ<>(~nLBK`OyNOE1U97*%!nWqjX{V|)Po)vQ7B_>5JLq83N3A2J>xyz?f!R@ z?WHzu8*Z8Eil#d^(4d+%xGY_FAe-&ij!o>n_lne>p|N7e4q8QRYE)6ws7>s>)t*6( z+M{Z(q9{shZ$)dXtzY{7zJEP&dm>Nn`<&}s!(1I^7z%^yEV^b{219 zv0@iz`+Y_0=$oz0WmHEc>w-+@mQ*C4Oqez{dHG5R;5a{9{4y<}+yt4ogQpu*AR)Wf zZAzbk#QB%BZ#0T`@mOCZ^N3+aX;ZgPQL&29duMkCoY{S3O?!#CQd>UxMl#*=8&$%` zh3$lP-39V2ktd<09g!Gm;e2Y7ic1BVLQ(HeglPK3k|PuWA`?H#t<=>~HS6|oEehuC zr4N7DN7aoxzke<;^JW4hnJQ;V;^viTrk#4%f~xyu!`teT)z*X%!wJ6Dd14&%Q7yG{ z_jv_+WPbAKd@}F%_{BiwfAO6vlHAgFeFPnLWgF?l1S|}rW6q2y9cviS36NNswCE3d0>v0pJbc%SqTF)*aKKk8f;=>(?V4@W{ zHk(xNIQ}^JXI>|$S9=BwS52|R*u$M?_8N%a(>?c73ZM|%2Me79SaK$`^st#PD_>0e z{{`d8!#XS!LUUN(DX*Ts+8$ zJnMy(JAK&rLb=<^$ETN}+t9TQgl(wjmG8|Ej`)5raznWx=soIUwav~D`nEJH@sZ<< z@s*{$C6IE7>hTV*$-uZ8B2z^6)rLhXBLJN0bX-3U9ZWw%` z2n=iz^dky|47%-~!><*z!pW36jz`}3DS8>?%5SCk1NLT6Gi&QBfvhd$mO6Tcy&bCgR(%awfbUs6OC5=Cq$#nEtzhIEe9nIXhX-p&7B=xD ze}d}(w`V-d?2hJFmp*zMb`tx(;bqh5-zT4Q=DwaU=L(A6@Ov8qlrl2<5RP_ar<{yf zev$qckZ2}!!sJ+x{%E@z#Oo^Ro}MnHRkA~Tb9yl0ihSTrv{C%AWu-Q`^DHce@Kr8X zo=@}n#nrVHdbK4vW{GW}c&6CyFJu{}IDi=HyN5bKKJYYux~FeKf%M-uOvqne5F~i> z_Fc+&uo4NKNcq>yMIQmNaSf9P&Uy{3RgeQgjR)vonOx;Q+@SL50D8gV1pSY;ohKvv z3GZNy7tThXOx{0*mBjhKQ^hD4OUaKRpkWE{3+j}-`*Dq*OwVWNkQNh&i9MpejjEGx z6cFEnYw#l-qy;~`r=OXXjOI%VSGJQr@?5NU5Zr4h7P!0dVd*jp4 z!PQ%1?)28Dfz?XLk!jV7(N(uzrvfd6)HKk^oJKwVExjyTq%UmmI$r=eP2B1tm*bz& zilecyqXyl?oU|v}JV9S#8G#&>KNNCg}{P_X7B0XtoeNT&c_W3+a5fAk{s2c-9qBVY9f5MaWc4`2ETQlz^ zd_RGmF`lgvzxj;>DK_y~E^zqju$Uxpkf{)=1d_Y3#=g>MGz#op3}|@^nRlA7OMg}W zs7k2PU;fTel0Z_Uq4a%ZpT<113-&$1V8RhszH!0&y5uixtB2#$8%X{`?s}JtlrAkK z0j0URdNTYW8vvxruiqlG%s`ZW*Rn{Gqa0r?mj+bW=-cFi&KjQHvtoo5jr1jcq;~GL zstkFFM$3{miaf}uHOyFk%Baox2CNd}>SrZV{~L^o6s@7jPO4G7iVz?gQ6o^bsfc@$ zo<`YY%ji4!=|&X|v#F!?s+O+|M^`FdrE{^*FT?O0ZMil1)Di9BT-o}60j1g_!(1Q^ zuC+j=hg!qmzRH8&^Cn)reUxHkAEkopoAKhDntiyEYCk!1w&)RJG=>|5P3WE$X34Wj zzeQ@ngH{7#^AK^0#&NYouF71CF$Rsrpv zgYLSp!x=-t#-Kp|ZzbB{-;HC9h_t1d(gWEs>QSdZVkJ&+^6<%D30r%e>^g6d5Vx_4 zQ5zRVe*^i>7HD&L{d(i9RFj)90$qxa4ZfT07zTP_8yr~i5kY)SpN00%qZ8G-h)-Xcx;EY~&FcRK;##L6 zY@P3J-c#!F&Eh;3m_b6O>_8a88B-}qD#5*$(=NslRuQEHF&2@8;IH#y#6u^4#$M9D z6-?qt!X$9f2 zGpz(wb(c!>tMVT?EqrSdOl*Q=V+ls)d#u+2fvI zb!rgY90%u-)fRw?-C&M)?tA)wqcAjlTS_BGlT@QJ@d=vvQ8#?ub*vycBJ`<+i6?kV zUjO#;wuG2#Acd4pU0<*KO)!Z)i4<3gj~3S1#azIU`&TwhxLziox*IUTV#)5_6$!-Y z<4h``OAP9eHYF#Srl##m@{~}WpuC`8eD4{&rgx&3bGJ))r}GINVA~fgAu)l@<0>!q zYQFX{RhnL*v)G8R>Aw4rv}MUCoiY$MWbX-WziYpL**&oJH1aRt48NkWgrBs$+6aCt zT1J=PrWg%!D6qwOdo{LNRoU&`2k%0F3 zTIXZqL5W_aaLFI!uUbw*BpLh;Lb9VtOp{{SK{#P2*TIKRID)oI20~p}y>tBC=utdz zvVm-U&lJFsVwrtji9wavg$ep~k&3&Q>jRQ&x?_2tEUQXHZ71u}4)7*|k|{%UN^_Bn z3cBNvB#_sXIa27}H$BMEgvyG5&0O#$w5HD7Y+y4g$(Dw6_*+1s!UWYC!5RhbJJ$M3 zsehw;Y+H-d%&%w(vs{FPfNKZ54{)75Gj>$Eq1BKb1z?FKFYkM#gU^`gNZ<4%2{uh| zmKOkZk^Cvb@eE&?*1qz>|DJ!*JV0xf>1mVy&C| zble_$7DsVk_ObB*nHDHyB~~@xtn2p}Nn=SB&yziT*A&7c>03L17;rR#vS(J2To#IF zUa|?<5=d_d?R)+wP1nf?d~bg@?lC`k2rM1du#TXjx05c;_7Yf1b=aW9+hoWS7TC<* zx0>4Wd!h58S+mTMgx@&qrnByk{dbqmhdTKsQKk}(8_X2McZB_4Kv1BM>F~W#$@~Nb z(|c{3{dW-~WWk~KbBO`XXZ=SEuGF8c*hhv+cMQ{x%i5A`I(aM^b?W8a>EIr+TA-7q zO(8yR60Ry4#=E*{;j|sUEIpka+nL55c;s+@wI?)!B6rdNDE={fR)YDRLj7zQCWrs< zZGUtOlBc~dIJTn7yUUp-yVBFEB8Y}eQcjhXZyhvxhOog=5CYfW$T2^2)LTpq~ zqpGM4rj#7wC`eUKjh8oWd=q6q$?^}M{ z_k4!WLs|T7EbiLLENNrHanMHIm(J~VzI-SA{m&J1IH))db?OL|;wKnl?!2sfbkBHUc z6H;Z8$129wEw$NRo2El{r^gtzT*>;A=)-$M-*FQbwt7X?bxk_H{Ae%g{ZS^Zg+VD6 z(H{&$glgO)IEaXOhc#Ua`iExia1mxyY?0jzadWj_3l_SQMk4(jRTMp$J=#BmIP+Z{ z>Kv9!RsyGIxeR4Q0j(xZaS$JtcIF0Tc$KRtbbNCrDE^A9KTZ35s!$x<;>Z5wZgV^+ z>8iSICY5Uq30@1|H#Rv}AtVpTq}d;xXYeKySC?m#_PFtr8*GT9hZpXw$!BQVZSQ{6 znR_u{ss3D7j=%s512LP#j~lMNdAS3Xw(e6D=2yoSi{YkLdrUjcVLkCNitt-%%a(^C zCYKk>NkY*!PdGD>Ro>wQp}5Ww01dphigpS~Njpk=gMyGO&<+`{Z>;Y+w%&yO^qL|o zlu3JR6kM^9Dn&lC6ewq`SE9}afoIAb$V%i2e0@lsCTc*N1B?gSyf%l?4yAg2C+5;! zoxZXGbrzlLBt_$^3u4JPbVF#+lWVPtgrERucN`9S>lPl!6s|CI!?7HRSc1C(u`Dhp zGyc^EG+Y}?kyQ(wqG2XLTNyyCE+<8xJfpu`%1bm?Fqx_faaZV^#+(g?*=8T^LwBJ5 zH_B@b`=4=+dvzb@o%;{Hq{r3I=+B8ZRV{BBQXNtoiJ#zq@WsJTFGPFZBh zJcosZfB>j#B#|09IymY2b!^_9f{M}8^=B;y?slND3U9+mMmMTTySIzWXKa89t};!a z7<6Zf)Le?Ge%rKdz!{T%yh zGt6e{>!eF1M#EV+{bCrcP5{I{%5SdvGgU&heLw@lUi#cxrxcojy$ysuF*Q^a&1x<* z^Sl6r?S{~QF2UD-e+nie1Yp>CE99H3&WQ|wW_Qeg@G#Ds`pf5n^xLZ|3-blXbvhpg zJ5H}E^eY#55UKacq6Fjzu1svdbG#hf2AHCdZ+h)~B~*RBj;ExtYgaPCqjed4d{Bzm z6OHm1)^P5333qMJN&Sq`Qqw+%iEENdn#UhaNPn7EEX0hXWQ*VbGObXNgP%^_cS zF#8pP*dhoc!>9Xvwa#H+9Uk!hYi0W2mFpdwt5sF7K4c3w-{0n4kOP0--Qvr$jz5fR zzT?XmkQ8=w531CW;~ONBD7O$ks!JI_yjMUX;v@V`Oi=&UxjT(Z^h++)v$u8u-}rcR zAFKtWTKT4-^%StZf&kPxkr_=^I6KSy=F6l+b>SbsZcv~JI;uk!IYAun&ssPDt+N7t z8V+?Sd=lIeG5>CTJ`WS1{~6m0VHm}8Q*|MQY?)N6@kvx~KiO*zZVl1+6+ZV0;gtsu z;UDt_K>}VkFA#k&Bl!Lgw_y2!7DQq94IHj+)Jwzm@pN}9kTbjVs~Y$?GSz+2wXr?J zuc~JAWA2XcwzT3%zUDU)aPl82CiqH?NyZHU9MeoQhCN`bqOZ1fd%jc^V&TRf3~rS= zsDk=-@h@^M{H`4LC@pEy{P|oaEh0x<7@xK^Bry`94m-hzmUL=*{E6~yekG*_Z{=IW zCu%d%rH)c>TI~N5r`ey~(^hsgdra>cDQo}K<2j^{BlEGH28Npv4O02awCDzdW-vIH zq5k)IRH}NDi`dadlC1~Wi_JY$c?8eGGtujQCQMU0jYFVDCwJ}x1x8KGS!h*ej_>n< z;N&s=+W?UPD9716m;v@tWWaK-?g(|Xi3BoU%+0k9m^1&_WlIPmja!F>4tpTrYbmZ= z0k;oWn1kGp96zxuiXLRZqolgRZ|H9?H@f^1$T@&{zG~4*u~DhM-Ln2&ew$JH8(>xe zda&G!%;nA52`UhE=B(WQt$5)?>Zte}Y4+T%<^&a|(&}VUBqwi?V{va`{k09@^J3qCkuziRX zdm*;_{SfhvwzLQ8@d{d0cz>Lfto0Y*x}X>yk~PbIKJwywaI?ghQWk<{9!6 z8_vSFuzo4L;pNjSD~jOU(Ed0Ie69$YA*5j?xANz?2k2nk9BJ?jIZqy)$B}Uk|2oAQ zqpd7Lve7uDbSawQ4$ZXfWF)bpayo8!lNme-pm!PVi@>Q>w z;cO{E`(nFw@`2oNLm_h!BWOFI>U!wBrL+Y;zjEB^F|FP1cMuvq%T5DUb6IBJPETeA zUT<#$s(QT&DO~21W6$RLhpT-_n6Rx@jd?0;iyEGM458^P*qyOp;^;XJjmCVO>fh$y z%b-zSA;AV7Fe_mF;4d@5F?p4LM0|d6df#^|IDs~ePgy+V1~YtLx_Z0j$zUnmmEWb` zD&gH=MWSC(iOVz8UsEo94$(vZSg*iHbK-a`0SnKddgZcJTljPqoyR?-fc#}+V>r>7 zp04<*bl`OMig52->`I}Pdu;q}=r3R|ks^VLS7`>-C%YoE4qM6;5<8aF}9PB$!Q@tP;O_azxYj z!}bpHLh zP%rpWO~4~)R&7Z@H$JF>ArfEXh?zgo{w6?JLxi7PJLU}7EAQ57Pjkj963P1D`e1t9 zuO{;I`LLfW+Hf7BSK^$Q!l^i)JvTGq?|Ix8p`W$ShLymI?1cIU#ex3$VmXMai|tPM zLuG}F{%Np&&iW?iQ;Gc3E=4gcci##v0u-=j@9GzT8C3tn#+iq zrN3AnqMzd>-UCZ7&rC$Ejz@Um3t0r-0wozbgasSNL!iGWmlN1kGaDJKQ<5Z-d6RFa z{R0d^0@N;A^e3mgV;NK+mQ4CzS6789kqNEylj|Y(|7kr7Ga5eCa^y@#YMwvH87tAO zIa|!K>ko-@9|PfLCO+XoF3LjbsDphHX-KGbDK^gM;+!7_kIu-!2<|L0?_f!<>8_iB zxIbQ#Ii(8>3;d&1HKR@$88a?5)216Ha0OAz=md60N*9rWlQ?z=7nwpv;kR01W=xOG zrsJBrH_w!Csxql+JFBX`A<+)qO&**pZ=ChMQFNEuyT-JbyLA7OWVT>Pp}|mR8((n^ z5J4`qyhd{KtYet`6nB2Xu*9S&j7|1TWl_j{K!v&Cb(oOab~Xms1V4nl^mR5?k9GB0 zHm%RAxzD|(kL6&5(v|Fjzt}ULzx8Y;FmOYqC_k!FZKYxH^VmxuL)!{J;R7_2p$sN# zI80szIXM2w$a}__P})`=1&4=|kWXcw-+xH@2DDF}BRV%r^T6p?H7g06-rg^LQ>j9+ zO_58^T7#h+tFp0b?>6Ofs*=C%pU=QRv@0=s_QFG}DGkw3Tvf!yl?PX+MVis2;;!(i zx(^W6Eso}nrZjhdMigm2^Cfh2)^ax+AKaYvI2NQAAB{9C^qBSYUQ|-deF|>LTiYD$ zqIR#uHN!r`^tj_!mH>mS(zMI8Y=_Yc50{ zRRLXBZ{lnq^NDj5_55&hdmJ{R!4@2A+R#&;3ijir^Z_m|XRoj6$>_$To7Nz~$OO(^ zI&H=DlGypfAb*gaN{%h4!YUxu63lNcC*PTURiQU-KWjwqJO*@BCIBYs3c>^fhs{W- zC>S|+lHL-UaKt*?S)qZd{`Im%p>_+hBc_I_^&$P7@k zO7q{bETe#}5excT(lb4XASo2E`Pc}b`+|E^?9%~h3^lAp8GsCRQYGn)a}^}{s>w7u8s={Pg=>XT9rYJeAR-V zTISM*NFR1%lg)L!$tPDLdFGmGb8gyJcNRVOHxQd?ZXL?|Gvw`hlt# z=3j+;mQN=~NibY+hmO6bUrF4~UT=0vOYJwGx@K8&TP}UodI_Hmg4^GO$SdK^Q@Z^z zdOQ;S?PrDb^$WmYhM~OMM~a(-72;3rCOiCNqj1rPqJnA25Fty6X0-tHMVyw#?d@6a zQ(c`Pxv43P&X%HTp(gyR}bx0EAtnCh30LR zf@br-&T}I90KMT$1l?(7OQeF){Y5s_T--@*qqyKCa;KE#bG%L0%sMUkxyiMWCso^h4cO|YI`4hJaAn`k^q+3BuG}Af zt!e&eO?7OKnhf&{1}xQ9<*fgpFJ{m>-{HeF(m>r8c`t{m7u?Ly=Obcc|xt73J59#8i{V*uOIf1K}uwmaxcAaDSi%xw3a<_@G9!>>o}? zr8~bPV#i*aJqgtSP*|f2?n`SO+)0oJ*oaKZ;3yLkQ0yC7>G8oP$`zg&aX^*aKPbT1Yl5cM}fj9j*t?X-u%uX?8wMqX0 z0?kj$x{10LwQW1k)J5y91=9X^l3&RX1PkrnSx(WNwGwbl+4ERGl>D+{A7yZ2!JCFY z;yH71x!p%^TG);T?}qYpx_tCXWug%)@ zX19^Lg2yKqRI0}#o^#r7&3Y+@q)xB9)}(~W{{_&7p)b6+fmD}|f5PVeKL-{Q1%}ow0(X9zM28LvD7ne$s5lDu;vYgVr}2YMfBiv$1FEWMnO>EFH#A@|iro;gSI@;E z(TTjXlz2rpb$y8OgjknG7si|lu*$-F&-!1*=%H$*>5KZr*`0*2r|>Bg!(w1{itmkL zl7HMrifdT}4<%Wo=qN+gkqeXRLBC|LaN47vMyVg}I8bkX2noqlXePJ||L`hQ(_m9O z48)>(?9e}$l?01a)Qu?19ZZ;MA_xJ@1=kpi#mdt;c(}D7(z&0y^ib8>F*V5wTsn=^ ztmKH^pKcT8egDn)QKSIzl=~aEAP98Xz0dLDV0Rly!ozW(o=g8;%SG@BIt8@yk@c>#*V+xBtSIV*(VbTQ6b5HxHyeqlqpp;Ja%c~(}FZqZD9xdC>wiZeaLmQ zL*Q4id$Cpsa%RAm>hND~il9qe0ljx{nU=Yi@aU7)spM#dj#yR^^6^stD3>%-Ut8{G zK)Y};k@4c;-Lab@@uWJ@^wN_}x!ZY7&)KwneGn5&ce1uy2qW~fM0DfpCSv>w^+lyD zeZlIc!c@G%R%FdXy{N`FqEbHFSNYF7F&)*PJmE_|Ja#t$86s)ge4?5FwixS2XjXmH zRDn_{e-u=1eZ6p7R)-2ChEA`CGO<5@QMBu<{XnNzB%k~1p4u3(^pc|3N{_2x!SB3J z<>dmC2p$Lfhc8k-o0aZ2;tRX&QX$Zen?)uj0Gt{W6MXx&TK(oU!UY;^I~mCn{`jp( zmKcKUR*(vLM!rv^7UvkE-S{rpB!+ajUF7pHE@C5Z>9Se~7q%=x=HE~&NC3v_*G>Nf zbthXVbIKOI9nh$^_jY-@IQ150UR(sR zyQ;)h@)cCTGy4=YVuR(nKh@E(&d?XEzB!9#HM0v6M152M?ykT;QneWI2Jcz&7VXLT zxc;-EW<7{Y4A`AcGB*thPf2CHBfgV~S4BHrk6jWTj3pnXVIY{f62?Qy3tDdv{N>ih z>mL0%!nX-lw3lY|9~zi}U@LaD$`kr~_1|MW6x>>_Zs6uypldUG(VKMMXaNCprUwf* zx31v70Nwi!-2-vMLDwZtuNQ{3aE&M{3etSm{{s41&soAYSPx=2w||7;zGm1XTNvsJ z66#K%y>h)KRXibnV8c7pdDi>>t;mzLP$U=Qc#;5@Ml@5WYdO=)bz=_z)oFJwXG_ZB zQvMPcX{r-(o+G%gPAI~eYl53!Kc*iR6oDJn;(v`{PEd?i|HKgOPHwkP&5?Znkw)0= zbGlvqI$rti6Sz?b!DSX28>r%gly zi~TZ?X*0V1@|$j*zrzRSDjCVq7y!YB$h5TiUx1(EtH45PfXBVU<>0W|RlK0Iayp2N z%8!Dd=j-JW#70`2;NUk7B^kbv**-+kS5pyg`a}Z__Lh~JlJfUpHa`!lK%^9X(Wgzr z@^Szp@UvycSFbOxoos9}OrCzXTS;=jtr8_O`xQOaBT|@kFthyf2}4#gMz*^Ix0XQH zI6-XG_T^*&6r|q`cFL9zl6*VQvHIA#Jh%KxuVlaJ!Dzdtsp=F@uy8S#a2O0y0|fT^Sb{{$Rp5?#HgGR!`$O?BGdT-{ctp; zv+u@6OjkZ0x+eO1X`mvFyw?Hy6nZ{?J?pSWP2|My5xGR4CH4YbY4|6KOnxM%K)%sM zytmMn+<0NmaFpW2)Fwt(NbI!hQvegYf=AusnL7R?5BP0g(L`msMb~4;*BP-^Z~}F( z%mQ9@5X7(uFXbj`$1$BOdHbj3z*j_Q20Af-_mexX>7uC*g$zMo-NlKIul;z(%?;NJ zZJjjgQgj^n8G7ruuZsxN6v3HIK&AGnJsw3|sBaK(!*nTZEh;%@wvo695Q36=5wf7h zAPBd{4Zb;_AHTAKh0px(h$l;ev{NX*MPqGHkRIzPA$ph}fS#8^{>RH#kNXQiwQ13R ziXRw;3jAm|V-+q@I`7V)nK{Z%WUYgG00D1QoUoCKyP~px0TzoS9VNUezB5|mUY#jq zFCUy4W7w#AV^>I@<%+As2NWdsU-u#DxuIgdXf*r-Tb|2~aFN1Ld(i&oepVhiYxO zd26N@Ro#k%7BWUC%BZd!A5J*j#W#?-Y)5gRUKjY|_lRn(@g&ldvHn^JqcpEiJvL*E zU2W5*9mbDBt`l0I;EfsbpcXnADPq>z^^!qZXimt9OTw z%%^t>&xCzcdU-3Gr6)oKNZ@8^vjYRnbnx7+{_ip>r za#!;xl54kTzU}uPWmA1!G;=+jnBOfvODZ8Btfr7iKn=OGSUE;oSS+#P+E!sYjm%GE zyKl8E+(io+r3){l&HNOi=20$9ILzbKXu=343y&+dBvcy_$VCUQ-8cOZfa{$Z?wiJT z|CxS#EHS;qqaNLt;WG22o9Nox*oF^QSoXb76FdsHaOud%-RveeTM#9hf0nN{Gy zSO5LkL1>~fY0%1%19LZx*uUF%PJ+%?guAIH_LIb6{rGeR?DIB3Gon#_y#hN{E1?Q^ z<(7_fJ-W>Ug1y8|j{9l4o*`)9^ny$}W@?{T58LDqi}~3fciY0H7i7;DV+}S$G((&b zl#ZOH+IOxSK`)^orti;1!y<$>UHQ(gtze1o!P9>MKunksnh~V?TvvS$Vi>_s_(-kk z8&6)tVxdJr$gWt?fjqIAp!l3+?Xv#capmWyl^stL_eBk!dE~-PH4x=5!cFJrXUyoV z--BfI&euy{yVEr{UXxi}Gssj44mV&tnuaN04R5mAkc${i(MsEym*+*%fiX}4Xk69#Xb&ai zMrSJyYv&SWD0zWB@yii-FT%oLHZ0Wrh73E2tX)?s5-2ckzxmZp^zpiy(&sOr@cIsx9-oeK5xH-4vB~TbMh9Gx+*S*0K7{R_Fixmx>cFC$ zbx(AK7;k)-h&G7vD91GKV5{L8$pO#!Bhvl zf_Xi+@2T|yp7;w>-sy!X66NZ`;3tunU&bAMN=~Pffiz7C-j}BRWO8MJznva$afU<| z$+TVSOmavqxCms@*rtw|clRcEguBC8+YEFaKuOCT^0_Y-S{x(sk>t3Fy!oMWLo%t% zfXR*qk1)-MwPFe9BMesea>u@_w?kV8Z!OeMZoYhA(y~UMrYwpU%f51i&vNvdyYKGLqo z?Zb_PDvV_uW}d4Bp0LB#id zj#bpqVBnROLibp~FSjUI@^>&TbWO*pusmgr-QdPm#(9^i`GhhebWAqUB8%;}PjwN; zi44{p;Pwj1T!D6GQ8Ft{5651aZ_<5qb^7oh`c0;%w|(1){hD{ezf=6IcNT5IgSOV1 zRqJ74x2foR=E1DYjW#Tk^9}E0yM1_gDCQL3vYYPj4lG}?#y|Tu+bcGEAL;3xxK4PyqUGFYE$h5s_%Z3VBv@MpG7lZ=zOLTd9R?|4v>FeKG0Yr>v=%}1N}c1V4qer zE|+$~|AX&j{qVyqCa79j4H6?v+`<1L3`oT&TDz8lbG@iZ1hPZl2~Ko;)50Qyix!iVE;&dBK6dY@w%Qn%rTe;@p2{W;)8 zfkihz1P2zT@1Vz0^gwx|AjjcH+BUc1BM$oBpX(?};WDCDv9~_IH53_wWOT<5EsNJq za4Y%6X9uR6H8s|6jX2}*%_ry8^cwi7e>xKHJ6!V{X{?{$7G;a$bVoT&5-u zO*ecf|7exSt&a*cRPnAZz@`!)1}7#6)h@M_PU)l&Olz0*l9e_--JVA~kMv_2C^LOi ziueT2BxYnK=wG2|4#9z#Id7X4cS(h)cJC*2P}EYj)kr&Oou7Q_xES3)N0XG%6fjKj zQqsOvWxyE`_R`BfxtgG38mgK2dhSlA8KaG=eRn?n^`|=v$NFk!C$;1*1p#&#n>(X! z!C5&K1vR5cv-61a-V*1nzoTXp<#Ia7)rsDGZA<-tmIfSR_1uVZnxq8n(%+E1W7I@1 z3!cNMPyuVD5UiV6ajd-SO(arcE3(l_JeYNl&f<>n+9DU-CN}?W=QviiQt1HrP{zl> z7G0oY98Y6ysniIW(JWlqH#R?wcaWI0y&&_zOvU=pMfzZ96ON=BSPQZ)2X2 zj_@*Dzii=z#*&Gw!gnu?zNpH?kBd*fqMY=N#P--~*{2!X!I2N7MzYQ;dX#(^gEMly zp}W~`;;5XYk{j(|PMa~*N=n(FqBkL&#yjX2KVZbD!oS;$6R2cz_oo~YOOk(7*JG1d z;W^VBdE<8-xaPO;>akG@&@-{^@`BF7;Gtl@HQd(vVC)jG;!9Z>eS?3|i#?rl>o?%I zt)|NF)9GZSOs1?H>yfCNIhI;zq|m;?Ne?Urf&T1HHe#k`L8H`#g;0?5)cK|n)>ok&kdhC>PK?UW z``-||u*jtlYZ9P39Dns*wi;9;UoHZ5UhK3fz$5WI;r3p!!>i^EY{%wR^HuOUiXxsF z%YY!O^_R3h^hV8u6{p)Jees{QO@qC)h7D%)M+z?8qwGF!Sgr@JaazBgUw%q}OYOm? z+BgIJcMoWw^wz{9jOLWKZWW)QM~0M{G%|ve%+kOYmQqMnXu#B7u)2-}N*Qcx73uzo z(#Q7qb)GsuK!c-FaUHpc8g}ejUEs%-v+m)!Vc>tLD*&Na#BDbatr5cIxXxP;nNNi& zH^D$qqWCf}o&43in~AQIDRk2qmycROAM4F!Toed`1pjbn^pe2&S8QqVzGnwgiD`>K zE(N2(af~&;^X?HVSnw%4QKvD8GYH+T$4sXX@a3zG)veBuU?z$25r)%dP?S;RVV~0G zciwfnzCt|YcUiQhpu}c}S2LMOJFT?YDAKqMI*X#Y$jY;aCP(WkZZGH}$v3}h71k(H zuE{;zeJE0BD&k|nm~*{EgS(Qv`a5zydT646T#}{!Ks4>AuElWXbYVqB?jOQ9E zV|?CtxOCd+G>U)RKmW))LUJ!Ib0ftaYn(B8TWv zH;)McIh!ZCFFqPaXF8a17G7H7|2t#Pkg{^Tm;J$pA{F%$RBt&eF5?)eEDMVZrsegf zoM*4kcjtjrFaHB7^aP3_gG0eL>fIT1p$Yg;O1&em$A&QNUCPEA>Obn**9WLmj%9a0 z@*46Uu$OmpTK*Y2mPBQA!03vf&7HI@1bh0JrzF}Z{INT1VI=^rf!(p;xy2Q#ronOJ zF=RvI$z9c4sM_86+(nF15Y8C8$|bmL&A00bavVkS^tU|C+szywe(pQ0p&fEGKf$J< zG~9I&9+TOV&SiW;cqf3GC$huWlQ9(!RgGlQa3cmjNpZymP}Zs6qsUIeghzvSSGx4p z+GMKcxE{8=!(eesTNjBL-+jv$9G%(RI`A>K2Hfceo|S=&r@jv?aXh1`h{|g+-jQY z4EgqJ0U6XZ5d%qlo|?H0XVq+H{8d%4A{x&&P?7`;iZTv1%aik@!pj|n#);xpL&FpJ z&aXRYW8ZUeBXqF`QE4l87njy@cN>-}j|$mc(&g>KFidrYkFsb zX-A8>zucWmi3_F&%J3Hc3=kKa?8zDqG7cJTuI?s7M{ynCOx9RE&m`}9$Ep;hxmrv6 zO-=iy<0yHCfz{p~1JzPaH7UWh=P+WkN?LK!u=va~SeqR@Ay~k?BEbg`ig}B&pbRug z-r~)U)5xR(kQ^(QeRR6(d{&y3l#=;g=Iro2X5x!J;@zx*-NOfpzkm(fk&7dFvpB}r zwuY~6w>;ws`M`YB$0XezXp!NaR`ydFk1KSNpQL#=nhG7s*d|u$e4M(z<<`}bueQVb zFYqsT@=q=kTwgsKoEMfAC82pl7HwzId`vKP8B#`n&&%7kzW{V8vs!#R73Pj_^&Sho z;K$b~7;xlo9*pjhT%i-ulo%|d#rfMNuXCQZoWB4hbPRu}#+AH}QSoBVcR}v@+#|LU zD(#);dnO5d_qc(E30Bk5^X6?J8=(Sr;;dKl$N{UL6_X0KD@)!vh+QKzY9aZS`=0F^ zlNIx6(o1VRRMsWoz#7aZ+eyAQ(997x!$?Z=?oB6KOC=!~n7{|^)Ty5tUBOBb({i%x z;ly8+ZpZ{}WioL|Sa5e^Rxd1S*-?rbd63Nf>qig-H zEZFMZtcgXbODWvU7Kw>Jo1a>ndf461q`;}-p{{8Yp4}}}baHuP6@nYiI?Fys(8pwI zd;EEu?ezBAXMIDNTA=x)608+4(QO`h2W@6j2J!olQQ=E*Irv=#$gCX~5)LCdBtr4p zeAbo5^auQUjj*US-@|i^Vk2y{eOZZv;?%#(6e3uB9i#~jKlJR7>n-6vt7akQhPYWMg|;ixG& z%aNx_r@sgb7aH6`>kvL<>)MkliPS~r|950Eyugb*>Z%Vtv$q`8GW%S>3JY-a2?H!o zn1V5|Pt>A4Q@U00&T3yJ$? zc8UrweUw)>lXZU4^-ooTgI^)b>3l&b?dLNhHlFVu5fMHbmFix(gQl0oX<(CcY)H`Q z6+TW|#y4s%Slng~Fl6;JaNj!sETfXyv{B1H{r0qN7>FSLJs~uciJH68D{5Z}(C~h9Yerqw zT|x?3U@9{|JnGLB*+7pOax(Axczs+Sr11U%&=#6y(aiLli<~1iv-{BDN%WE8p=*K8 z&H{{tswugaWBlhSBDHp0GU(u+IEFS8MZW<&z`rK|$tr~~TIOS54SF^ZqCWr&1KMkA zYs*5Q+?NZsEXa9^-~b2;fA&P~Ly=3%BGaEi``>nTvpi+G2!q*^!e@NG?2hVJx=Zel zKMT*SJnp~S-ozhH9$Kv2C;bZ$70EbZ9V;;Y;iJt3js}UFecE7hrA`1=eL&kuzbAtd z>~2e4*@!e(z2iuLHuxPstv0urHs_t@;Q1O@mH40)XdKDAC4EB|Pdsx8@0q3V${jhF z!4>B-g@qc`D5@x@*vltAvgYqq*pM|yW7o$zB^sPNlbl_Mb>$wQs=*G$N2qsRu-v=R zrT%;WH0~dl4&TfDaN^ykGAtAO{%O!x0FWRus%24-z_w{H9~vGDCFt9#>17o>54V3a zRW_c>HwEhXseIQx$I>=Xw_pKmn{ZJ@$~1X1GN_;)=h0i`J_c+1EIweLE5-&pBT+J-E% znHrStOT=y{D-UIqo)A=G+*Uhyi7oObTZxQLagG@r)%O7JEpJW?j`P^al&h4WW_(;; zVujn=XM5(8_aP_hJ-k5K~b~p-%a* zBX{S=Bm?n0jBG^v^elKC)%SWmkz_2>at4g-O0f=WL06L#95E_V(v4SU0&Ej4y4slj z0;0F~*}a$9Jl!g|v@mmYGuVb-vtIT%>1LBz^68=R%#nLG@NS<1%jQuo#2OOSly8&- zNFcP2T$iuVH8c`BgJUYgv0oE(wcOku`y_r8AoH%VRPuU+6_rw(U$R>_1Ed=jWy#O~ zz`XD+-R*No-RkW{9O=Kbu8>3RA1FV?)Lu}R1+F64A z=^T4uLdTYbn=vOe(7}-G{P|Ue&D@L2A9Tzx%pHf z!-BS=QA!e(mYP6din|~@pJjuJY7W89XVi>0gV03{K|hpMt{<|)U=`jr=op79CYt}Y zP^p)u^7-RmWTHW^G}oG1=(RPJg(}CSM4^ghGd^iu@4Z%Ap7`%C zkzzvrZxs*Zcgq%9TxguEzE#of*bB!}^;V;#&r2Ua$X}Bn0B&E`U5o2uIn_5WexC@F zv1PWGopHWB@HVtohB>FL4Yr=xp?=44AFutR%Bn;2|J>o5))3mVtdkSC@5KE`|MKHZ zpkhW)47fI-!b>~&SzSfOE1N^DOetUOKDP(Ye3ifis)tVf)dKEMHId`&3aaGyJk%-p z(96N>N#*?eTq2oa?I(b3f43gsBw$O4+#8L+=S)>K{-AQ#Z}FL$4+|X^1t~;iet-D8 zD$%L8C#&E1vjwDEQ1AbXfB9)HMrjY9Wo}D;+&}*EH1Hth?8SHQyU c_f{vay_+lGvC-21Kf`;Y!;M%=SNZ=p0adU_0RR91 literal 0 HcmV?d00001 diff --git a/dash-fastapi-backend/config/env.py b/dash-fastapi-backend/config/env.py index d602163..fbfb944 100644 --- a/dash-fastapi-backend/config/env.py +++ b/dash-fastapi-backend/config/env.py @@ -1,3 +1,6 @@ +import os + + class JwtConfig: """ Jwt配置 @@ -27,3 +30,11 @@ class RedisConfig: USERNAME = '' PASSWORD = '' DB = 2 + + +class CachePathConfig: + """ + 缓存目录配置 + """ + PATH = os.path.join(os.path.abspath(os.getcwd()), 'caches') + PATHSTR = 'caches' diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index ddeac6c..5ef2342 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -79,7 +79,6 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope else: error_msg = result_dict.get('message') if log_type == 'login': - # print(request.headers) user_agent_info = parse(user_agent) browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' diff --git a/dash-fastapi-backend/module_admin/controller/common_controller.py b/dash-fastapi-backend/module_admin/controller/common_controller.py new file mode 100644 index 0000000..5be2b13 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/common_controller.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, UploadFile, File, Form +from fastapi.responses import StreamingResponse +from config.env import CachePathConfig +from module_admin.service.login_service import get_current_user +from module_admin.service.common_service import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth + + +commonController = APIRouter() + + +@commonController.post("/upload", dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) +async def common_upload(request: Request, uploadId: str = Form(), file: UploadFile = File(...)): + try: + try: + os.makedirs(os.path.join(CachePathConfig.PATH, uploadId)) + except FileExistsError: + pass + upload_service(CachePathConfig.PATH, uploadId, file) + logger.info('上传成功') + return response_200(data={'filename': file.filename}, message="上传成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@commonController.get(f"/{CachePathConfig.PATHSTR}") +def common_download(request: Request, taskId: str, filename: str): + try: + def generate_file(): + with open(os.path.join(CachePathConfig.PATH, taskId, filename), 'rb') as response_file: + yield from response_file + logger.info('获取成功') + return StreamingResponse(generate_file()) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 6876ba4..0a91b56 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -15,7 +15,7 @@ loginController = APIRouter() @loginController.post("/loginByAccount", response_model=Token) -@log_decorator(title='用户登录', business_type=0, log_type='login') +# @log_decorator(title='用户登录', business_type=0, log_type='login') async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): try: result = authenticate_user(query_db, user.user_name, user.password) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index bfaafef..f58be54 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, Request from fastapi import Depends, Header +import base64 from config.get_db import get_db +from config.env import CachePathConfig from module_admin.service.login_service import get_current_user, get_password_hash from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * @@ -91,3 +93,78 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + avatar = edit_user.avatar + # 去除 base64 字符串中的头部信息(data:image/jpeg;base64, 等等) + base64_string = avatar.split(',', 1)[1] + # 解码 base64 字符串 + file_data = base64.b64decode(base64_string) + dir_path = os.path.join(CachePathConfig.PATH, 'avatar') + try: + os.makedirs(dir_path) + except FileExistsError: + pass + filepath = os.path.join(dir_path, f'{current_user.user.user_name}_avatar.jpeg') + with open(filepath, 'wb') as f: + f.write(file_data) + edit_user.user_id = current_user.user.user_id + edit_user.avatar = f'{request.base_url}common/{CachePathConfig.PATHSTR}?taskId=avatar&filename={current_user.user.user_name}_avatar.jpeg' + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) + else: + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/changeInfo", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) +async def change_system_user_profile_info(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_user.user_id = current_user.user.user_id + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) + else: + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/resetPwd", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) +async def reset_system_user_password(request: Request, reset_user: ResetUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if not reset_user.user_id: + reset_user.user_id = current_user.user.user_id + reset_user.password = get_password_hash(reset_user.password) + reset_user.update_by = current_user.user.user_name + reset_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + reset_user_result = reset_user_services(query_db, reset_user) + if reset_user_result.is_success: + logger.info(reset_user_result.message) + return response_200(data=reset_user_result, message=reset_user_result.message) + else: + logger.warning(reset_user_result.message) + return response_400(data="", message=reset_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index 7c9c700..94cfe66 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -207,6 +207,13 @@ class AddUserModel(UserModel): type: Optional[str] +class ResetUserModel(UserModel): + """ + 重置用户密码模型 + """ + old_password: Optional[str] + + class DeleteUserModel(BaseModel): """ 删除用户模型 diff --git a/dash-fastapi-backend/module_admin/service/common_service.py b/dash-fastapi-backend/module_admin/service/common_service.py new file mode 100644 index 0000000..a0bb413 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/common_service.py @@ -0,0 +1,14 @@ +import os +from datetime import datetime +from fastapi import UploadFile +from config.env import CachePathConfig + + +def upload_service(path: str, upload_id: str, file: UploadFile): + + filepath = os.path.join(path, upload_id, f'{file.filename}') + with open(filepath, 'wb') as f: + # 流式写出大型文件,这里的10代表10MB + for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''): + f.write(chunk) + diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 230de75..4c68e2d 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -1,5 +1,6 @@ from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * +from module_admin.service.login_service import verify_password def get_user_list_services(result_db: Session, page_object: UserPageObject): @@ -47,13 +48,13 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): :return: 编辑用户校验结果 """ edit_user = page_object.dict(exclude_unset=True) - if page_object.type != 'status': + if page_object.type != 'status' and page_object.type != 'avatar': del edit_user['role_id'] del edit_user['post_id'] - if page_object.type == 'status': + if page_object.type == 'status' or page_object.type == 'avatar': del edit_user['type'] edit_user_result = edit_user_dao(result_db, edit_user) - if edit_user_result.is_success and page_object.type != 'status': + if edit_user_result.is_success and page_object.type != 'status' and page_object.type != 'avatar': user_id_dict = dict(user_id=page_object.user_id) delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) @@ -106,3 +107,21 @@ def detail_user_services(result_db: Session, user_id: int): role=user.user_role_info, post=user.user_post_info ) + + +def reset_user_services(result_db: Session, page_object: ResetUserModel): + """ + 重置用户密码service + :param result_db: orm对象 + :param page_object: 重置用户对象 + :return: 重置用户校验结果 + """ + user = get_user_detail_by_id(result_db, user_id=page_object.user_id).user_basic_info[0] + if not verify_password(page_object.old_password, user.password): + result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + else: + reset_user = page_object.dict(exclude_unset=True) + del reset_user['old_password'] + result = edit_user_dao(result_db, reset_user) + + return result diff --git a/dash-fastapi-frontend/api/user.py b/dash-fastapi-frontend/api/user.py index 2edf88d..4ca0f5d 100644 --- a/dash-fastapi-frontend/api/user.py +++ b/dash-fastapi-frontend/api/user.py @@ -29,3 +29,18 @@ def delete_user_api(page_obj: dict): def get_user_detail_api(user_id: int): return api_request(method='get', url=f'/system/user/{user_id}', is_headers=True) + + +def change_user_avatar_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/changeAvatar', is_headers=True, json=page_obj) + + +def change_user_info_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/changeInfo', is_headers=True, json=page_obj) + + +def reset_user_password_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/resetPwd', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index c827f04..ff353f4 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -82,9 +82,6 @@ def router(pathname, trigger): current_user_result = get_current_user_info_api() if current_user_result['code'] == 200: current_user = current_user_result['data'] - user_name = current_user['user']['user_name'] - nick_name = current_user['user']['nick_name'] - phone_number = current_user['user']['phonenumber'] menu_list = current_user['menu'] user_menu_list = [item for item in menu_list if item.get('visible') == '0'] menu_info = deal_user_menu_info(0, menu_list) @@ -94,7 +91,7 @@ def router(pathname, trigger): session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] valid_href_list = find_node_values(menu_info, 'href') - valid_href_list.append('/') + valid_href_list = valid_href_list + RouterConfig.STATIC_VALID_PATHNAME if pathname in valid_href_list: current_key = find_key_by_href(menu_info, pathname) if trigger == 'load': @@ -102,6 +99,8 @@ def router(pathname, trigger): # 根据pathname控制渲染行为 if pathname == '/': current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' if pathname == '/login' or pathname == '/forget': # 重定向到主页面 return [ @@ -119,7 +118,7 @@ def router(pathname, trigger): # 否则正常渲染主页面 return [ - views.layout.render_content(user_name, nick_name, phone_number, user_menu_info), + views.layout.render_content(user_menu_info), None, fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), {'timestamp': time.time()}, @@ -132,6 +131,8 @@ def router(pathname, trigger): else: if pathname == '/': current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' return [ dash.no_update, None, diff --git a/dash-fastapi-frontend/assets/css/cropper.min.css b/dash-fastapi-frontend/assets/css/cropper.min.css new file mode 100644 index 0000000..e97743a --- /dev/null +++ b/dash-fastapi-frontend/assets/css/cropper.min.css @@ -0,0 +1,9 @@ +/*! + * Cropper.js v1.5.13 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2022-11-20T05:30:43.444Z + */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} \ No newline at end of file diff --git a/dash-fastapi-frontend/assets/js/cropper.min.js b/dash-fastapi-frontend/assets/js/cropper.min.js new file mode 100644 index 0000000..03aed4c --- /dev/null +++ b/dash-fastapi-frontend/assets/js/cropper.min.js @@ -0,0 +1,10 @@ +/*! + * Cropper.js v1.5.13 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2022-11-20T05:30:46.114Z + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Cropper=e()}(this,function(){"use strict";function C(e,t){var i,a=Object.keys(e);return Object.getOwnPropertySymbols&&(i=Object.getOwnPropertySymbols(e),t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),a.push.apply(a,i)),a}function S(a){for(var t=1;tt.length)&&(e=t.length);for(var i=0,a=new Array(e);it.width?3===i?o=t.height*e:h=t.width/e:3===i?h=t.width/e:o=t.height*e,{aspectRatio:e,naturalWidth:n,naturalHeight:a,width:o,height:h});this.canvasData=e,this.limited=1===i||2===i,this.limitCanvas(!0,!0),e.width=Math.min(Math.max(e.width,e.minWidth),e.maxWidth),e.height=Math.min(Math.max(e.height,e.minHeight),e.maxHeight),e.left=(t.width-e.width)/2,e.top=(t.height-e.height)/2,e.oldLeft=e.left,e.oldTop=e.top,this.initialCanvasData=g({},e)},limitCanvas:function(t,e){var i=this.options,a=this.containerData,n=this.canvasData,o=this.cropBoxData,h=i.viewMode,r=n.aspectRatio,s=this.cropped&&o;t&&(t=Number(i.minCanvasWidth)||0,i=Number(i.minCanvasHeight)||0,1=a.width&&(n.minLeft=Math.min(0,r),n.maxLeft=Math.max(0,r)),n.height>=a.height)&&(n.minTop=Math.min(0,t),n.maxTop=Math.max(0,t))):(n.minLeft=-n.width,n.minTop=-n.height,n.maxLeft=a.width,n.maxTop=a.height))},renderCanvas:function(t,e){var i,a,n,o,h=this.canvasData,r=this.imageData;e&&(e={width:r.naturalWidth*Math.abs(r.scaleX||1),height:r.naturalHeight*Math.abs(r.scaleY||1),degree:r.rotate||0},r=e.width,o=e.height,e=e.degree,i=90==(e=Math.abs(e)%180)?{width:o,height:r}:(a=e%90*Math.PI/180,i=Math.sin(a),n=r*(a=Math.cos(a))+o*i,r=r*i+o*a,90h.maxWidth||h.widthh.maxHeight||h.heighte.width?a.height=a.width/i:a.width=a.height*i),this.cropBoxData=a,this.limitCropBox(!0,!0),a.width=Math.min(Math.max(a.width,a.minWidth),a.maxWidth),a.height=Math.min(Math.max(a.height,a.minHeight),a.maxHeight),a.width=Math.max(a.minWidth,a.width*t),a.height=Math.max(a.minHeight,a.height*t),a.left=e.left+(e.width-a.width)/2,a.top=e.top+(e.height-a.height)/2,a.oldLeft=a.left,a.oldTop=a.top,this.initialCropBoxData=g({},a)},limitCropBox:function(t,e){var i,a,n=this.options,o=this.containerData,h=this.canvasData,r=this.cropBoxData,s=this.limited,c=n.aspectRatio;t&&(t=Number(n.minCropBoxWidth)||0,n=Number(n.minCropBoxHeight)||0,i=s?Math.min(o.width,h.width,h.width+h.left,o.width-h.left):o.width,a=s?Math.min(o.height,h.height,h.height+h.top,o.height-h.top):o.height,t=Math.min(t,o.width),n=Math.min(n,o.height),c&&(t&&n?ti.maxWidth||i.widthi.maxHeight||i.height=e.width&&i.height>=e.height?U:P),f(this.cropBox,g({width:i.width,height:i.height},x({translateX:i.left,translateY:i.top}))),this.cropped&&this.limited&&this.limitCanvas(!0,!0),this.disabled||this.output()},output:function(){this.preview(),y(this.element,_,this.getData())}},i={initPreview:function(){var t=this.element,i=this.crossOrigin,e=this.options.preview,a=i?this.crossOriginUrl:this.url,n=t.alt||"The image to preview",o=document.createElement("img");i&&(o.crossOrigin=i),o.src=a,o.alt=n,this.viewBox.appendChild(o),this.viewBoxImage=o,e&&("string"==typeof(o=e)?o=t.ownerDocument.querySelectorAll(e):e.querySelector&&(o=[e]),z(this.previews=o,function(t){var e=document.createElement("img");w(t,m,{width:t.offsetWidth,height:t.offsetHeight,html:t.innerHTML}),i&&(e.crossOrigin=i),e.src=a,e.alt=n,e.style.cssText='display:block;width:100%;height:auto;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation:0deg!important;"',t.innerHTML="",t.appendChild(e)}))},resetPreview:function(){z(this.previews,function(e){var i=Dt(e,m),i=(f(e,{width:i.width,height:i.height}),e.innerHTML=i.html,e),e=m;if(o(i[e]))try{delete i[e]}catch(t){i[e]=void 0}else if(i.dataset)try{delete i.dataset[e]}catch(t){i.dataset[e]=void 0}else i.removeAttribute("data-".concat(Ct(e)))})},preview:function(){var h=this.imageData,t=this.canvasData,e=this.cropBoxData,r=e.width,s=e.height,c=h.width,d=h.height,l=e.left-t.left-h.left,p=e.top-t.top-h.top;this.cropped&&!this.disabled&&(f(this.viewBoxImage,g({width:c,height:d},x(g({translateX:-l,translateY:-p},h)))),z(this.previews,function(t){var e=Dt(t,m),i=e.width,e=e.height,a=i,n=e,o=1;r&&(n=s*(o=i/r)),s&&eMath.abs(a-1)?i:a)&&(t.restore&&(o=this.getCanvasData(),h=this.getCropBoxData()),this.render(),t.restore)&&(this.setCanvasData(z(o,function(t,e){o[e]=t*n})),this.setCropBoxData(z(h,function(t,e){h[e]=t*n}))))},dblclick:function(){var t,e;this.disabled||this.options.dragMode===J||this.setDragMode((t=this.dragBox,e=$,(t.classList?t.classList.contains(e):-1y&&(D.x=y-f);break;case k:p+D.xx&&(D.y=x-v)}}var i,a,o,n=this.options,h=this.canvasData,r=this.containerData,s=this.cropBoxData,c=this.pointers,d=this.action,l=n.aspectRatio,p=s.left,m=s.top,u=s.width,g=s.height,f=p+u,v=m+g,w=0,b=0,y=r.width,x=r.height,M=!0,C=(!l&&t.shiftKey&&(l=u&&g?u/g:1),this.limited&&(w=s.minLeft,b=s.minTop,y=w+Math.min(r.width,h.width,h.left+h.width),x=b+Math.min(r.height,h.height,h.top+h.height)),c[Object.keys(c)[0]]),D={x:C.endX-C.startX,y:C.endY-C.startY};switch(d){case P:p+=D.x,m+=D.y;break;case B:0<=D.x&&(y<=f||l&&(m<=b||x<=v))?M=!1:(e(B),(u+=D.x)<0&&(d=k,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case T:D.y<=0&&(m<=b||l&&(p<=w||y<=f))?M=!1:(e(T),g-=D.y,m+=D.y,g<0&&(d=O,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case k:D.x<=0&&(p<=w||l&&(m<=b||x<=v))?M=!1:(e(k),u-=D.x,p+=D.x,u<0&&(d=B,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case O:0<=D.y&&(x<=v||l&&(p<=w||y<=f))?M=!1:(e(O),(g+=D.y)<0&&(d=T,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case E:if(l){if(D.y<=0&&(m<=b||y<=f)){M=!1;break}e(T),g-=D.y,m+=D.y,u=g*l}else e(T),e(B),!(0<=D.x)||fMath.abs(o)&&(o=i)})}),o),t),M=!1;break;case I:D.x&&D.y?(i=Et(this.cropper),p=C.startX-i.left,m=C.startY-i.top,u=s.minWidth,g=s.minHeight,0 or element.");this.element=t,this.options=g({},mt,u(e)&&e),this.cropped=!1,this.disabled=!1,this.pointers={},this.ready=!1,this.reloading=!1,this.replaced=!1,this.sized=!1,this.sizing=!1,this.init()}var t,e,i;return t=n,i=[{key:"noConflict",value:function(){return window.Cropper=jt,n}},{key:"setDefaults",value:function(t){g(mt,u(t)&&t)}}],(e=[{key:"init",value:function(){var t,e=this.element,i=e.tagName.toLowerCase();if(!e[c]){if(e[c]=this,"img"===i){if(this.isImg=!0,t=e.getAttribute("src")||"",!(this.originalUrl=t))return;t=e.src}else"canvas"===i&&window.HTMLCanvasElement&&(t=e.toDataURL());this.load(t)}}},{key:"load",value:function(t){var e,i,a,n,o,h,r=this;t&&(this.url=t,this.imageData={},e=this.element,(i=this.options).rotatable||i.scalable||(i.checkOrientation=!1),i.checkOrientation&&window.ArrayBuffer?dt.test(t)?lt.test(t)?this.read((h=(h=t).replace(Yt,""),a=atob(h),h=new ArrayBuffer(a.length),z(n=new Uint8Array(h),function(t,e){n[e]=a.charCodeAt(e)}),h)):this.clone():(o=new XMLHttpRequest,h=this.clone.bind(this),this.reloading=!0,(this.xhr=o).onabort=h,o.onerror=h,o.ontimeout=h,o.onprogress=function(){o.getResponseHeader("content-type")!==st&&o.abort()},o.onload=function(){r.read(o.response)},o.onloadend=function(){r.reloading=!1,r.xhr=null},i.checkCrossOrigin&&Nt(t)&&e.crossOrigin&&(t=Lt(t)),o.open("GET",t,!0),o.responseType="arraybuffer",o.withCredentials="use-credentials"===e.crossOrigin,o.send()):this.clone())}},{key:"read",value:function(t){var e=this.options,i=this.imageData,a=Xt(t),n=0,o=1,h=1;1

',o=(n=n.querySelector(".".concat(c,"-container"))).querySelector(".".concat(c,"-canvas")),h=n.querySelector(".".concat(c,"-drag-box")),s=(r=n.querySelector(".".concat(c,"-crop-box"))).querySelector(".".concat(c,"-face")),this.container=a,this.cropper=n,this.canvas=o,this.dragBox=h,this.cropBox=r,this.viewBox=n.querySelector(".".concat(c,"-view-box")),this.face=s,o.appendChild(i),v(t,L),a.insertBefore(n,t.nextSibling),X(i,K),this.initPreview(),this.bind(),e.initialAspectRatio=Math.max(0,e.initialAspectRatio)||NaN,e.aspectRatio=Math.max(0,e.aspectRatio)||NaN,e.viewMode=Math.max(0,Math.min(3,Math.round(e.viewMode)))||0,v(r,L),e.guides||v(r.getElementsByClassName("".concat(c,"-dashed")),L),e.center||v(r.getElementsByClassName("".concat(c,"-center")),L),e.background&&v(n,"".concat(c,"-bg")),e.highlight||v(s,Z),e.cropBoxMovable&&(v(s,G),w(s,d,P)),e.cropBoxResizable||(v(r.getElementsByClassName("".concat(c,"-line")),L),v(r.getElementsByClassName("".concat(c,"-point")),L)),this.render(),this.ready=!0,this.setDragMode(e.dragMode),e.autoCrop&&this.crop(),this.setData(e.data),l(e.ready)&&b(t,"ready",e.ready,{once:!0}),y(t,"ready"))}},{key:"unbuild",value:function(){var t;this.ready&&(this.ready=!1,this.unbind(),this.resetPreview(),(t=this.cropper.parentNode)&&t.removeChild(this.cropper),X(this.element,L))}},{key:"uncreate",value:function(){this.ready?(this.unbuild(),this.ready=!1,this.cropped=!1):this.sizing?(this.sizingImage.onload=null,this.sizing=!1,this.sized=!1):this.reloading?(this.xhr.onabort=null,this.xhr.abort()):this.image&&this.stop()}}])&&A(t.prototype,e),i&&A(t,i),Object.defineProperty(t,"prototype",{writable:!1}),n}();return g(Pt.prototype,t,i,e,Rt,St,At),Pt}); \ No newline at end of file diff --git a/dash-fastapi-frontend/callbacks/layout_c/head_c.py b/dash-fastapi-frontend/callbacks/layout_c/head_c.py index f93b772..d686f91 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/head_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/head_c.py @@ -1,6 +1,7 @@ import dash from dash import dcc from flask import session +import time from dash.dependencies import Input, Output, State from server import app @@ -9,7 +10,7 @@ from api.login import logout_api # 页首右侧个人中心选项卡回调 @app.callback( - [Output('index-personal-info-modal', 'visible'), + [Output('dcc-url', 'pathname', allow_duplicate=True), Output('logout-modal', 'visible')], Input('index-header-dropdown', 'nClicks'), State('index-header-dropdown', 'clickedKey'), @@ -18,17 +19,17 @@ from api.login import logout_api def index_dropdown_click(nClicks, clickedKey): if clickedKey == '退出登录': return [ - False, + dash.no_update, True ] elif clickedKey == '个人资料': return [ - True, + '/user/profile', False ] - return dash.no_update + return [dash.no_update] * 2 # 退出登录回调 diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 91bb76a..801f310 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -49,10 +49,15 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act currentKey ] - menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) - button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] - # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 - menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) + if currentKey == '个人资料': + menu_title = '个人资料' + button_perms = [] + menu_modules = 'system.user.profile' + else: + menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) + button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] + # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 + menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) if menu_modules: # 否则追加子项返回 @@ -116,7 +121,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act # 页首面包屑和hash回调 @app.callback( [Output('header-breadcrumb', 'items'), - Output('dcc-url', 'pathname')], + Output('dcc-url', 'pathname', allow_duplicate=True)], Input('tabs-container', 'activeKey'), State('menu-info-store-container', 'data'), prevent_initial_call=True diff --git a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py index 143eb2c..233b2b6 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py @@ -42,7 +42,8 @@ def get_login_log_table_data(search_click, pagination, operations, ipaddr, user_ page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'login_log-list-table': query_params = dict( ipaddr=ipaddr, user_name=user_name, diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py index eb26438..3032fe8 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -44,7 +44,8 @@ def get_operation_log_table_data(search_click, pagination, operations, title, op page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'operation_log-list-table': query_params = dict( title=title, oper_name=oper_name, diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py index 4ab9899..5a09b33 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py @@ -42,7 +42,8 @@ def get_dict_type_table_data(search_click, pagination, operations, dict_name, di page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'dict_type-list-table': query_params = dict( dict_name=dict_name, dict_type=dict_type, diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py index 6ea7dd8..a1d06c4 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py @@ -34,7 +34,8 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'dict_data-list-table': query_params = dict( dict_type=dict_type, dict_label=dict_label, @@ -81,8 +82,7 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di @app.callback( - [Output('dict_data-dict_type-select', 'value', allow_duplicate=True), - Output('dict_data-dict_label-input', 'value'), + [Output('dict_data-dict_label-input', 'value'), Output('dict_data-status-select', 'value'), Output('dict_data-operations-store', 'data')], Input('dict_data-reset', 'nClicks'), @@ -90,9 +90,9 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di ) def reset_dict_data_query_params(reset_click): if reset_click: - return [None, None, None, {'type': 'reset'}] + return [None, None, {'type': 'reset'}] - return [dash.no_update] * 4 + return [dash.no_update] * 3 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 3a6fdee..73ad53c 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -34,7 +34,8 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'post-list-table': query_params = dict( post_code=post_code, post_name=post_name, diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 87ad59e..fde15bd 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -43,7 +43,8 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'role-list-table': query_params = dict( role_name=role_name, role_key=role_key, diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py new file mode 100644 index 0000000..e74c31c --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py @@ -0,0 +1,154 @@ +import dash +import feffery_utils_components as fuc +import time +import uuid +from dash.dependencies import Input, Output, State +from server import app + +from api.user import change_user_avatar_api + + +@app.callback( + [Output('avatar-cropper-modal', 'visible', allow_duplicate=True), + Output('avatar-src-data', 'data', allow_duplicate=True)], + Input('avatar-edit-click', 'n_clicks'), + State('user-avatar-image-info', 'src'), + prevent_initial_call=True +) +def avatar_cropper_modal_visible(n_clicks, user_avatar_image_info): + if n_clicks: + return [True, user_avatar_image_info] + + return dash.no_update, dash.no_update + + +@app.callback( + Output('avatar-src-data', 'data', allow_duplicate=True), + Input('avatar-upload-choose', 'listUploadTaskRecord'), + prevent_initial_call=True +) +def upload_user_avatar(list_upload_task_record): + if list_upload_task_record: + + return list_upload_task_record[-1].get('url') + + return dash.no_update + + +@app.callback( + Output('avatar-cropper', 'jsString'), + Input('avatar-src-data', 'data'), + prevent_initial_call=True +) +def edit_user_avatar(src_data): + + return """ + // 创建新图像元素 + var newImage = document.createElement('img'); + newImage.id = 'user-avatar-image'; + newImage.src = '% s'; + newImage.onload = function() { + // 删除旧图像元素 + var oldImage = document.getElementById('user-avatar-image'); + oldImage.parentNode.removeChild(oldImage); + // 销毁旧的 Cropper.js 实例 + var oldCropper = oldImage.cropper; + if (oldCropper) { + oldCropper.destroy(); + } + // 将新图像添加到页面中 + var container = document.getElementById('avatar-cropper-container'); + container.appendChild(newImage); + // var image = document.getElementById('user-avatar-image'); + var previewImage = document.getElementById('user-avatar-image-preview'); + // 创建新的 Cropper 实例 + console.log(cropper) + var cropper = new Cropper(newImage, { + viewMode: 1, + dragMode: 'none', + initialAspectRatio: 1, + aspectRatio: 1, + preview: previewImage, + background: true, + autoCropArea: 0.6, + zoomOnWheel: true, + crop: function(event) { + // 当裁剪框的位置或尺寸发生改变时触发的回调函数 + console.log(event.detail.x); + console.log(event.detail.y); + console.log(event.detail.width); + console.log(event.detail.height); + console.log(event.detail.rotate); + console.log(event.detail.scaleX); + console.log(event.detail.scaleY); + // 当需要获取裁剪后的数据时 + var croppedDataUrl = cropper.getCroppedCanvas().toDataURL("image/jpeg", 1); + sessionStorage.setItem('cropper-avatar-base64', JSON.stringify({avatarBase64: croppedDataUrl})) + console.log(croppedDataUrl) + } + }); + // 获取旋转按钮的引用 + var rotateLeftButton = document.getElementById('rotate-left'); + var rotateRightButton = document.getElementById('rotate-right'); + + // 添加点击事件监听器 + rotateLeftButton.addEventListener('click', function() { + // 向左旋转图像90度 + cropper.rotate(-90); + }); + rotateRightButton.addEventListener('click', function() { + // 向右旋转图像90度 + cropper.rotate(90); + }); + // 获取缩小按钮和放大按钮的引用 + var zoomOutButton = document.getElementById('zoom-out'); + var zoomInButton = document.getElementById('zoom-in'); + + // 添加点击事件监听器 + zoomOutButton.addEventListener('click', function() { + // 放大图像 + cropper.zoom(0.1); + }); + + zoomInButton.addEventListener('click', function() { + // 缩小图像 + cropper.zoom(-0.1); + }); + } + """ % src_data + + +@app.callback( + [Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True), + Output('avatar-cropper-modal', 'visible', allow_duplicate=True), + Output('user-avatar-image-info', 'key'), + Output('avatar-info', 'key')], + Input('change-avatar-submit', 'nClicks'), + State('cropper-avatar-base64', 'data'), + prevent_initial_call=True +) +def change_user_avatar_callback(submit_click, avatar_data): + + if submit_click: + params = dict(type='avatar', avatar=avatar_data['avatarBase64']) + change_avatar_result = change_user_avatar_api(params) + if change_avatar_result.get('code') == 200: + + return [ + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + False, + str(uuid.uuid4()), + str(uuid.uuid4()) + ] + + return [ + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + dash.no_update, + dash.no_update, + dash.no_update + ] + + return [dash.no_update] * 5 diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py new file mode 100644 index 0000000..642b52d --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py @@ -0,0 +1,91 @@ +import dash +import feffery_utils_components as fuc +import time +from dash.dependencies import Input, Output, State +from server import app + +from api.user import reset_user_password_api + + +@app.callback( + [Output('reset-old-password-form-item', 'validateStatus'), + Output('reset-new-password-form-item', 'validateStatus'), + Output('reset-confirm-password-form-item', 'validateStatus'), + Output('reset-new-password-form-item', 'help'), + Output('reset-old-password-form-item', 'help'), + Output('reset-confirm-password-form-item', 'help'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('reset-password-submit', 'nClicks'), + [State('reset-old-password', 'value'), + State('reset-new-password', 'value'), + State('reset-confirm-password', 'value')], + prevent_initial_call=True +) +def reset_submit_user_info(reset_click, old_password, new_password, confirm_password): + if reset_click: + if all([old_password, new_password, confirm_password]): + + if new_password == confirm_password: + + params = dict(type='avatar', old_password=old_password, password=new_password) + reset_password_result = reset_user_password_api(params) + if reset_password_result.get('code') == 200: + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + ] + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None, + None if new_password else 'error', + None if confirm_password else 'error', + None, + None if new_password else '前后两次密码不一致!', + None if confirm_password else '前后两次密码不一致!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None if old_password else 'error', + None if new_password else 'error', + None if confirm_password else 'error', + None if old_password else '请输入旧密码!', + None if new_password else '请输入新密码!', + None if confirm_password else '请输入确认密码!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [dash.no_update] * 8 + + +@app.callback( + Output('tabs-container', 'latestDeletePane', allow_duplicate=True), + Input('reset-password-close', 'nClicks'), + prevent_initial_call=True +) +def close_personal_info_modal(close_click): + if close_click: + + return '个人资料' + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py new file mode 100644 index 0000000..6cdef31 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py @@ -0,0 +1,79 @@ +import dash +import feffery_utils_components as fuc +import time +from dash.dependencies import Input, Output, State +from server import app + +from api.user import change_user_info_api + + +@app.callback( + [Output('reset-user-nick_name-form-item', 'validateStatus'), + Output('reset-user-phonenumber-form-item', 'validateStatus'), + Output('reset-user-email-form-item', 'validateStatus'), + Output('reset-user-nick_name-form-item', 'help'), + Output('reset-user-phonenumber-form-item', 'help'), + Output('reset-user-email-form-item', 'help'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('reset-submit', 'nClicks'), + [State('reset-user-nick_name', 'value'), + State('reset-user-phonenumber', 'value'), + State('reset-user-email', 'value'), + State('reset-user-sex', 'value')], + prevent_initial_call=True +) +def reset_submit_user_info(reset_click, nick_name, phonenumber, email, sex): + if reset_click: + if all([nick_name, phonenumber, email]): + + params = dict(type='avatar', nick_name=nick_name, phonenumber=phonenumber, email=email, sex=sex) + change_user_info_result = change_user_info_api(params) + if change_user_info_result.get('code') == 200: + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + ] + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None if nick_name else 'error', + None if phonenumber else 'error', + None if email else 'error', + None if nick_name else '请输入用户昵称!', + None if phonenumber else '请输入手机号码!', + None if email else '请输入邮箱!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [dash.no_update] * 8 + + +@app.callback( + Output('tabs-container', 'latestDeletePane', allow_duplicate=True), + Input('reset-close', 'nClicks'), + prevent_initial_call=True +) +def close_personal_info_modal(close_click): + if close_click: + + return '个人资料' + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py similarity index 99% rename from dash-fastapi-frontend/callbacks/system_c/user_c.py rename to dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py index ef43a6e..e161a7a 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py @@ -70,7 +70,8 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'user-list-table': query_params = dict( dept_id=dept_id, user_name=user_name, diff --git a/dash-fastapi-frontend/config/global_config.py b/dash-fastapi-frontend/config/global_config.py index ea4ffd0..b1e5c3c 100644 --- a/dash-fastapi-frontend/config/global_config.py +++ b/dash-fastapi-frontend/config/global_config.py @@ -14,6 +14,9 @@ class RouterConfig: '/', '/login', '/forget' ] + # 静态路由列表 + STATIC_VALID_PATHNAME = ['/', '/user/profile'] + class ApiBaseUrlConfig: diff --git a/dash-fastapi-frontend/views/layout/__init__.py b/dash-fastapi-frontend/views/layout/__init__.py index c2ab0e6..fbb23ce 100644 --- a/dash-fastapi-frontend/views/layout/__init__.py +++ b/dash-fastapi-frontend/views/layout/__init__.py @@ -5,12 +5,11 @@ import feffery_antd_components as fac from views.layout.components.head import render_head_content from views.layout.components.content import render_main_content from views.layout.components.aside import render_aside_content -# import callbacks.index_c import callbacks.layout_c.fold_side_menu import callbacks.layout_c.index_c -def render_content(user_name, nick_name, phone_number, menu_info): +def render_content(menu_info): return fuc.FefferyTopProgress( html.Div( @@ -21,46 +20,18 @@ def render_content(user_name, nick_name, phone_number, menu_info): html.Div(id='idle-placeholder-container'), # 注入相关modal - html.Div( - [ - # 个人资料面板 - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdText( - user_name, - copyable=True - ), - label='账号' - ), - fac.AntdFormItem( - fac.AntdText( - nick_name, - copyable=True - ), - label='姓名' - ), - fac.AntdFormItem( - fac.AntdText( - phone_number, - copyable=True - ), - label='电话' - ) - ], - labelCol={ - 'span': 4 - } - ) - ], - id='index-personal-info-modal', - title='个人资料', - mask=False - ), - ] - ), + # html.Div( + # [ + # # 个人资料面板 + # fac.AntdModal( + # render_user_profile(), + # id='index-personal-info-modal', + # title='个人资料', + # width=1000, + # mask=False + # ) + # ] + # ), # 退出登录对话框提示 fac.AntdModal( @@ -105,7 +76,7 @@ def render_content(user_name, nick_name, phone_number, menu_info): fac.AntdCol( [ fac.AntdRow( - render_head_content(user_name), + render_head_content(), style={ 'height': '50px', 'boxShadow': 'rgb(240 241 242) 0px 2px 14px', @@ -117,7 +88,7 @@ def render_content(user_name, nick_name, phone_number, menu_info): } ), fac.AntdRow( - render_main_content(user_name, nick_name, phone_number), + render_main_content(), wrap=False ) ], diff --git a/dash-fastapi-frontend/views/layout/components/content.py b/dash-fastapi-frontend/views/layout/components/content.py index f5caf92..7d17211 100644 --- a/dash-fastapi-frontend/views/layout/components/content.py +++ b/dash-fastapi-frontend/views/layout/components/content.py @@ -2,7 +2,7 @@ from dash import html import feffery_antd_components as fac -def render_main_content(user_name, nick_name, phone_number): +def render_main_content(): return [ # 右侧主体内容区域 fac.AntdCol( @@ -26,7 +26,8 @@ def render_main_content(user_name, nick_name, phone_number): # defaultActiveKey='首页', style={ 'width': '100%', - 'paddingLeft': '15px' + 'paddingLeft': '15px', + 'paddingRight': '15px' } ), # id='index-main-content-container', diff --git a/dash-fastapi-frontend/views/layout/components/head.py b/dash-fastapi-frontend/views/layout/components/head.py index 5540e12..5651e87 100644 --- a/dash-fastapi-frontend/views/layout/components/head.py +++ b/dash-fastapi-frontend/views/layout/components/head.py @@ -1,10 +1,10 @@ -from dash import html +from dash import html, dcc import feffery_antd_components as fac - +from flask import session import callbacks.layout_c.head_c -def render_head_content(user_name): +def render_head_content(): return [ # 页首左侧折叠按钮区域 fac.AntdCol( @@ -57,14 +57,12 @@ def render_head_content(user_name): [ fac.AntdTooltip( fac.AntdAvatar( - mode='text', - size=36, - text=user_name, - style={ - 'background': 'gold' - } + id='avatar-info', + mode='image', + src=session.get('user_info').get('avatar'), + size=36 ), - title='当前用户:' + user_name, + title='当前用户:' + session.get('user_info').get('user_name'), placement='bottom' ), diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index e067f9f..b87ae59 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -1,7 +1,7 @@ from dash import dcc, html import feffery_antd_components as fac -import callbacks.system_c.user_c +from . import profile from api.user import get_user_list_api from api.dept import get_dept_tree_api diff --git a/dash-fastapi-frontend/views/system/user/profile/__init__.py b/dash-fastapi-frontend/views/system/user/profile/__init__.py new file mode 100644 index 0000000..d7c0de9 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/__init__.py @@ -0,0 +1,182 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac +from flask import session +from . import user_avatar, user_info, reset_pwd + + +def render(button_perms): + + return [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdCard( + [ + html.Div( + [ + html.Div( + user_avatar.render(), + style={ + 'textAlign': 'center', + 'marginBottom': '10px' + } + ), + html.Ul( + [ + html.Li( + [ + fac.AntdIcon(icon='antd-user'), + fac.AntdText('用户名称'), + html.Div( + session.get('user_info').get('user_name'), + id='profile_c-username', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-mobile'), + fac.AntdText('手机号码'), + html.Div( + session.get('user_info').get('phonenumber'), + id='profile_c-phonenumber', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-mail'), + fac.AntdText('用户邮箱'), + html.Div( + session.get('user_info').get('email'), + id='profile_c-email', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-cluster'), + fac.AntdText('所属部门'), + html.Div( + session.get('dept_info').get('dept_name') + "/" + ','.join( + [item.get('post_name') for item in + session.get('post_info')]), + id='profile_c-dept', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-team'), + fac.AntdText('所属角色'), + html.Div( + ','.join([item.get('role_name') for item in + session.get('role_info')]), + id='profile_c-role', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-schedule'), + fac.AntdText('创建日期'), + html.Div( + session.get('user_info').get('create_time'), + id='profile_c-create_time', + className='pull-right' + ) + ], + className='list-group-item' + ), + ], + className='list-group list-group-striped' + ), + fuc.FefferyStyle( + rawStyle= + ''' + .list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; + } + + .list-group { + padding-left: 0px; + list-style: none; + } + + .list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; + } + + .pull-right { + float: right !important; + } + ''' + ) + ], + style={ + 'width': '100%' + } + ), + ], + 'size="small"', + title='个人信息', + size='small', + style={ + 'boxShadow': 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px' + } + ), + span=10 + ), + fac.AntdCol( + fac.AntdCard( + [ + fac.AntdTabs( + items=[ + { + 'key': '基本资料', + 'label': '基本资料', + 'children': user_info.render() + }, + { + 'key': '修改密码', + 'label': '修改密码', + 'children': reset_pwd.render() + } + ], + style={ + 'width': '100%' + } + ) + ], + 'size="small"', + title='基本资料', + size='small', + style={ + 'boxShadow': 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px' + } + ), + span=14 + ), + ], + gutter=10 + ), + ] diff --git a/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py b/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py new file mode 100644 index 0000000..b7b15b2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py @@ -0,0 +1,66 @@ +import feffery_antd_components as fac + +import callbacks.system_c.user_c.profile_c.reset_pwd_c + + +def render(): + return fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-old-password', + mode='password' + ), + id='reset-old-password-form-item', + label='旧密码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-new-password', + mode='password' + ), + id='reset-new-password-form-item', + label='新密码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-confirm-password', + mode='password' + ), + id='reset-confirm-password-form-item', + label='确认密码', + required=True + ), + fac.AntdFormItem( + fac.AntdSpace( + [ + fac.AntdButton( + '保存', + id='reset-password-submit', + type='primary' + ), + fac.AntdButton( + '关闭', + id='reset-password-close', + type='primary', + danger=True + ), + ], + ), + wrapperCol={ + 'offset': 4 + } + ) + ], + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + }, + style={ + 'margin': '0 auto' # 以快捷实现居中布局效果 + } + ) diff --git a/dash-fastapi-frontend/views/system/user/profile/user_avatar.py b/dash-fastapi-frontend/views/system/user/profile/user_avatar.py new file mode 100644 index 0000000..f8b2341 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/user_avatar.py @@ -0,0 +1,177 @@ +from dash import html, dcc +import feffery_utils_components as fuc +import feffery_antd_components as fac +from flask import session + +from config.global_config import ApiBaseUrlConfig +import callbacks.system_c.user_c.profile_c.avatar_c + + +def render(): + return [ + dcc.Store(id='init-cropper'), + dcc.Store(id='avatar-src-data'), + # 监听裁剪的图片数据 + fuc.FefferySessionStorage( + id='cropper-avatar-base64' + ), + html.Div( + [ + fac.AntdImage( + id='user-avatar-image-info', + src=session.get('user_info').get('avatar'), + preview=False, + height='120px', + width='120px', + style={ + 'borderRadius': '50%' + } + ) + ], + id='avatar-edit-click', + className='user-info-head' + ), + fuc.FefferyStyle( + rawStyle=''' + .user-info-head { + position: relative; + display: inline-block; + height: 120px; + } + + .user-info-head:hover:after { + content: '+'; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + color: #eee; + background: rgba(0, 0, 0, 0.5); + font-size: 24px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + cursor: pointer; + line-height: 110px; + border-radius: 50%; + } + ''' + ), + fuc.FefferyExecuteJs(id='avatar-cropper'), + fac.AntdModal( + [ + fac.AntdRow( + [ + fac.AntdCol( + [ + html.Div( + [ + html.Img( + id='user-avatar-image', + height='120px', + width='120px' + ), + ], + id='avatar-cropper-container', + style={ + 'height': '350px', + 'width': '100%' + } + ), + ], + span=12 + ), + fac.AntdCol( + [ + html.Div( + id='user-avatar-image-preview', + className='avatar-upload-preview' + ), + fuc.FefferyStyle( + rawStyle=""" + .avatar-upload-preview { + margin: 18% auto; + width: 220px; + height: 220px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; + } + """ + ) + ], + span=12 + ) + ] + ), + html.Br(), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdUpload( + id='avatar-upload-choose', + apiUrl=f'{ApiBaseUrlConfig.BaseUrl}/common/upload', + downloadUrl=f'{ApiBaseUrlConfig.BaseUrl}/common/caches', + headers={'token': 'Bearer' + session.get('token')}, + fileMaxSize=1, + showUploadList=False, + fileTypes=['jpeg', 'jpg', 'png'], + buttonContent='选择' + ), + span=4 + ), + fac.AntdCol( + fac.AntdButton( + id='zoom-out', + icon=fac.AntdIcon( + icon='antd-plus' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + id='zoom-in', + icon=fac.AntdIcon( + icon='antd-minus' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + icon=fac.AntdIcon( + id='rotate-left', + icon='antd-undo' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + icon=fac.AntdIcon( + id='rotate-right', + icon='antd-redo' + ) + ), + span=7 + ), + fac.AntdCol( + fac.AntdButton( + '提交', + id='change-avatar-submit', + type='primary' + ), + span=7 + ), + ], + gutter=10 + ) + ], + id='avatar-cropper-modal', + title='修改头像', + width=850, + mask=False + ) + ] diff --git a/dash-fastapi-frontend/views/system/user/profile/user_info.py b/dash-fastapi-frontend/views/system/user/profile/user_info.py new file mode 100644 index 0000000..75ca5ab --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/user_info.py @@ -0,0 +1,84 @@ +import feffery_antd_components as fac + +import callbacks.system_c.user_c.profile_c.user_info_c + + +def render(): + return fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-nick_name', + placeholder='请输入用户昵称' + ), + id='reset-user-nick_name-form-item', + label='用户昵称', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-phonenumber', + placeholder='请输入手机号码' + ), + id='reset-user-phonenumber-form-item', + label='手机号码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-email', + placeholder='请输入邮箱' + ), + id='reset-user-email-form-item', + label='邮箱', + required=True + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='reset-user-sex', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + } + ], + defaultValue='1' + ), + id='reset-user-sex-form-item', + label='性别' + ), + fac.AntdFormItem( + fac.AntdSpace( + [ + fac.AntdButton( + '保存', + id='reset-submit', + type='primary' + ), + fac.AntdButton( + '关闭', + id='reset-close', + type='primary', + danger=True + ), + ], + ), + wrapperCol={ + 'offset': 4 + } + ) + ], + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + }, + style={ + 'margin': '0 auto' # 以快捷实现居中布局效果 + } + ) -- Gitee From 26e0d2448d34994afa00cd232949fcbb4e0a009e Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 14:16:44 +0800 Subject: [PATCH 11/54] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E8=B5=84=E6=96=99tab=E5=88=87=E6=8D=A2=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E4=B8=8D=E5=90=8C=E6=AD=A5=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/layout_c/index_c.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 801f310..a70b5b8 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -141,6 +141,21 @@ def get_current_breadcrumbs(active_key, menu_info): '/' ] + elif active_key == '个人资料': + return [ + [ + { + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/' + }, + { + 'title': '个人资料', + } + ], + '/user/profile' + ] + else: result = find_parents(menu_info.get('menu_info'), active_key) # 去除result的重复项 -- Gitee From b0280e84b7cb9478fc67428020780a05d6c4efb0 Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 15:09:13 +0800 Subject: [PATCH 12/54] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=9B=9E=E8=B0=83=E5=A4=B1=E6=95=88=E7=9A=84?= =?UTF-8?q?bug=E5=8F=8A=E5=85=B6=E4=BB=96bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/system_c/user_c/profile_c/reset_pwd_c.py | 2 +- dash-fastapi-frontend/views/system/user/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py index 642b52d..a5457d8 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py @@ -11,8 +11,8 @@ from api.user import reset_user_password_api [Output('reset-old-password-form-item', 'validateStatus'), Output('reset-new-password-form-item', 'validateStatus'), Output('reset-confirm-password-form-item', 'validateStatus'), - Output('reset-new-password-form-item', 'help'), Output('reset-old-password-form-item', 'help'), + Output('reset-new-password-form-item', 'help'), Output('reset-confirm-password-form-item', 'help'), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index b87ae59..0911d78 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -5,6 +5,8 @@ from . import profile from api.user import get_user_list_api from api.dept import get_dept_tree_api +import callbacks.system_c.user_c.user_c + def render(button_perms): dept_params = dict(dept_name='') -- Gitee From dfe198344492c219fcc2902b0b7d8aa70042ff60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Tue, 25 Jul 2023 14:34:01 +0800 Subject: [PATCH 13/54] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../controller/user_controller.py | 25 +++- .../module_admin/dao/user_dao.py | 140 ++++++++++++------ .../module_admin/entity/vo/user_vo.py | 4 +- .../module_admin/service/user_service.py | 18 ++- dash-fastapi-backend/utils/page_util.py | 45 ++++++ 6 files changed, 183 insertions(+), 51 deletions(-) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 6fd8a88..e705c7a 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -6,6 +6,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from module_admin.controller.login_controller import loginController +from module_admin.controller.captcha_controller import captchaController from module_admin.controller.user_controller import userController from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController @@ -74,6 +75,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): app.include_router(loginController, prefix="/login", tags=['login']) +app.include_router(captchaController, prefix="/captcha", tags=['captcha']) app.include_router(userController, prefix="/system", tags=['system/user']) app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index f58be54..dd6cb63 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -7,6 +7,7 @@ from module_admin.service.login_service import get_current_user, get_password_ha from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * +from utils.page_util import get_page_obj from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth @@ -17,11 +18,16 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): +async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db)): try: + # 拆分user_query = 分页类 + UserModel + user_query = UserModel(**user_page_query.dict()) + # 获取全量数据 user_query_result = get_user_list_services(query_db, user_query) + # 分页操作 + user_page_query_result = get_page_obj(user_query_result, user_page_query.page_num, user_page_query.page_size) logger.info('获取成功') - return response_200(data=user_query_result, message="获取成功") + return response_200(data=user_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -84,7 +90,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@userController.post("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) @@ -95,6 +101,19 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses return response_500(data="", message="接口异常") +# @userController.post("/user/export", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:expot'))]) +# @log_decorator(title='用户管理', business_type=5) +# async def export_detail_system_user(request: Request, export_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + # try: + # delete_user_result = detail_user_services(query_db, user_id) + # logger.info(f'获取user_id为{user_id}的信息成功') + # return response_200(data=delete_user_result, message='获取成功') + # except Exception as e: + # logger.exception(e) + # return response_500(data="", message="接口异常") + + + @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index 158da3b..f34330b 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -6,10 +6,11 @@ from module_admin.entity.do.dept_do import SysDept from module_admin.entity.do.post_do import SysPost from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ - UserPageObjectResponse, CrudUserResponse + UserPageObjectResponse, CrudUserResponse, UserInfoJoinDept from utils.time_format_util import list_format_datetime, format_datetime_dict_list from utils.page_util import get_page_info from datetime import datetime, time +from typing import Union, List def get_user_by_name(db: Session, user_name: str): @@ -110,51 +111,112 @@ def get_user_detail_by_id(db: Session, user_id: int): return CurrentUserInfo(**results) -def get_user_list(db: Session, page_object: UserPageObject): +# def get_user_list(db: Session, page_object: UserPageObject): +# """ +# 根据查询参数获取用户列表信息 +# :param db: orm对象 +# :param page_object: 分页查询参数对象 +# :return: 用户列表信息对象 +# """ +# count = db.query(SysUser, SysDept) \ +# .filter(SysUser.del_flag == 0, +# SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, +# SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, +# SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, +# SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, +# SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, +# SysUser.status == page_object.status if page_object.status else True, +# SysUser.sex == page_object.sex if page_object.sex else True, +# SysUser.create_time.between( +# datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), +# datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) +# if page_object.create_time_start and page_object.create_time_end else True +# ) \ +# .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ +# .distinct().count() +# offset_com = (page_object.page_num - 1) * page_object.page_size +# page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) +# user_list = db.query(SysUser, SysDept) \ +# .filter(SysUser.del_flag == 0, +# SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, +# SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, +# SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, +# SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, +# SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, +# SysUser.status == page_object.status if page_object.status else True, +# SysUser.sex == page_object.sex if page_object.sex else True, +# SysUser.create_time.between( +# datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), +# datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) +# if page_object.create_time_start and page_object.create_time_end else True +# ) \ +# .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ +# .offset(page_info.offset) \ +# .limit(page_object.page_size) \ +# .distinct().all() +# +# result_list = [] +# if user_list: +# for item in user_list: +# obj = dict( +# user_id=item[0].user_id, +# dept_id=item[0].dept_id, +# dept_name=item[1].dept_name if item[1] else '', +# user_name=item[0].user_name, +# nick_name=item[0].nick_name, +# user_type=item[0].user_type, +# email=item[0].email, +# phonenumber=item[0].phonenumber, +# sex=item[0].sex, +# avatar=item[0].avatar, +# status=item[0].status, +# del_flag=item[0].del_flag, +# login_ip=item[0].login_ip, +# login_date=item[0].login_date, +# create_by=item[0].create_by, +# create_time=item[0].create_time, +# update_by=item[0].update_by, +# update_time=item[0].update_time, +# remark=item[0].remark +# ) +# result_list.append(obj) +# +# result = dict( +# rows=format_datetime_dict_list(result_list), +# page_num=page_info.page_num, +# page_size=page_info.page_size, +# total=page_info.total, +# has_next=page_info.has_next +# ) +# +# return UserPageObjectResponse(**result) + + +def get_user_list(db: Session, user_object: UserModel): """ 根据查询参数获取用户列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param user_object: 分页查询参数对象 :return: 用户列表信息对象 """ - count = db.query(SysUser, SysDept) \ - .filter(SysUser.del_flag == 0, - SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, - SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, - SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, - SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.status == page_object.status if page_object.status else True, - SysUser.sex == page_object.sex if page_object.sex else True, - SysUser.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - ) \ - .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, - SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, - SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, - SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, - SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.status == page_object.status if page_object.status else True, - SysUser.sex == page_object.sex if page_object.sex else True, + SysUser.dept_id == user_object.dept_id if user_object.dept_id else True, + SysUser.user_name.like(f'%{user_object.user_name}%') if user_object.user_name else True, + SysUser.nick_name.like(f'%{user_object.nick_name}%') if user_object.nick_name else True, + SysUser.email.like(f'%{user_object.email}%') if user_object.email else True, + SysUser.phonenumber.like(f'%{user_object.phonenumber}%') if user_object.phonenumber else True, + SysUser.status == user_object.status if user_object.status else True, + SysUser.sex == user_object.sex if user_object.sex else True, SysUser.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(user_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(user_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if user_object.create_time_start and user_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result_list = [] + result_list: List[Union[UserInfoJoinDept, None]] = [] if user_list: for item in user_list: obj = dict( @@ -180,15 +242,7 @@ def get_user_list(db: Session, page_object: UserPageObject): ) result_list.append(obj) - result = dict( - rows=format_datetime_dict_list(result_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return UserPageObjectResponse(**result) + return result_list def add_user_dao(db: Session, user: UserModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index 94cfe66..c492d8a 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -32,6 +32,8 @@ class UserModel(BaseModel): update_by: Optional[str] update_time: Optional[str] remark: Optional[str] + create_time_start: Optional[str] + create_time_end: Optional[str] class Config: orm_mode = True @@ -156,8 +158,6 @@ class UserPageObject(UserModel): """ 用户管理分页查询模型 """ - create_time_start: Optional[str] - create_time_end: Optional[str] page_num: int page_size: int diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 4c68e2d..b070411 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -3,14 +3,26 @@ from module_admin.dao.user_dao import * from module_admin.service.login_service import verify_password -def get_user_list_services(result_db: Session, page_object: UserPageObject): +# def get_user_list_services(result_db: Session, page_object: UserPageObject): +# """ +# 获取用户列表信息service +# :param result_db: orm对象 +# :param page_object: 分页查询参数对象 +# :return: 用户列表信息对象 +# """ +# user_list_result = get_user_list(result_db, page_object) +# +# return user_list_result + + +def get_user_list_services(result_db: Session, user_object: UserModel): """ 获取用户列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param user_object: 分页查询参数对象 :return: 用户列表信息对象 """ - user_list_result = get_user_list(result_db, page_object) + user_list_result = get_user_list(result_db, user_object) return user_list_result diff --git a/dash-fastapi-backend/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py index 21ef67e..6b63ff8 100644 --- a/dash-fastapi-backend/utils/page_util.py +++ b/dash-fastapi-backend/utils/page_util.py @@ -1,5 +1,10 @@ +import math +from typing import List + from pydantic import BaseModel +from utils.time_format_util import format_datetime_dict_list + class PageModel(BaseModel): """ @@ -12,6 +17,17 @@ class PageModel(BaseModel): has_next: bool +class PageObjectResponse(BaseModel): + """ + 用户管理列表分页查询返回模型 + """ + rows: List = [] + page_num: int + page_size: int + total: int + has_next: bool + + def get_page_info(offset: int, page_num: int, page_size: int, count: int): """ 根据分页参数获取分页信息 @@ -39,3 +55,32 @@ def get_page_info(offset: int, page_num: int, page_size: int, count: int): result = dict(offset=res_offset, page_num=res_page_num, page_size=page_size, total=count, has_next=has_next) return PageModel(**result) + + +def get_page_obj(data_list: List, page_num: int, page_size: int): + """ + 输入数据列表data_list和分页信息,返回分页数据列表结果 + :param data_list: 原始数据列表 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :return: 分页数据对象 + """ + # 计算起始索引和结束索引 + start = (page_num - 1) * page_size + end = page_num * page_size + + # 根据计算得到的起始索引和结束索引对数据列表进行切片 + paginated_data = data_list[start:end] + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False; + + result = dict( + rows=format_datetime_dict_list(paginated_data), + page_num=page_num, + page_size=page_size, + total=len(data_list), + has_next=has_next + ) + + return PageObjectResponse(**result) + + -- Gitee From 176dbe713181e4b80dd847cc02b4a683136991a9 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Tue, 8 Aug 2023 16:56:18 +0800 Subject: [PATCH 14/54] =?UTF-8?q?refactor:=E9=87=8D=E6=9E=84=E6=89=80?= =?UTF-8?q?=E6=9C=89=E6=A8=A1=E5=9D=97=E7=9A=84=E5=90=8E=E7=AB=AF=E5=88=86?= =?UTF-8?q?=E9=A1=B5=20feat:=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20fix:=E4=BF=AE=E5=A4=8D=E8=A7=92=E8=89=B2=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=B6=8A=E6=9D=83=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 4 +- .../controller/dict_controller.py | 21 +++-- .../module_admin/controller/log_controller.py | 19 ++-- .../controller/menu_controller.py | 15 +-- .../module_admin/controller/post_controler.py | 10 +- .../controller/role_controller.py | 9 +- .../controller/user_controller.py | 7 +- .../module_admin/dao/dict_dao.py | 71 +++----------- .../module_admin/dao/log_dao.py | 84 ++++------------- .../module_admin/dao/menu_dao.py | 94 ++++++++++++++----- .../module_admin/dao/post_dao.py | 32 ++----- .../module_admin/dao/role_dao.py | 45 ++------- .../module_admin/dao/user_dao.py | 50 +++++----- .../module_admin/entity/vo/user_vo.py | 24 +---- .../module_admin/service/dict_service.py | 26 +++-- .../module_admin/service/log_service.py | 12 +-- .../module_admin/service/menu_service.py | 16 ++-- .../module_admin/service/post_service.py | 6 +- .../module_admin/service/role_service.py | 6 +- .../module_admin/service/user_service.py | 16 ++-- dash-fastapi-backend/utils/page_util.py | 6 +- dash-fastapi-frontend/app.py | 16 ++-- .../callbacks/system_c/dept_c.py | 68 ++++++++++---- .../callbacks/system_c/role_c.py | 27 +++--- .../callbacks/system_c/user_c/user_c.py | 89 +++++++++++++++--- .../views/system/dept/__init__.py | 57 +++++++---- .../views/system/role/__init__.py | 27 +++--- .../views/system/user/__init__.py | 57 ++++++----- 28 files changed, 488 insertions(+), 426 deletions(-) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index e705c7a..3cde6db 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from module_admin.controller.login_controller import loginController -from module_admin.controller.captcha_controller import captchaController +# from module_admin.controller.captcha_controller import captchaController from module_admin.controller.user_controller import userController from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController @@ -75,7 +75,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): app.include_router(loginController, prefix="/login", tags=['login']) -app.include_router(captchaController, prefix="/captcha", tags=['captcha']) +# app.include_router(captchaController, prefix="/captcha", tags=['captcha']) app.include_router(userController, prefix="/system", tags=['system/user']) app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py index 4634e06..4c92e70 100644 --- a/dash-fastapi-backend/module_admin/controller/dict_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -6,6 +6,7 @@ from module_admin.service.dict_service import * from module_admin.entity.vo.dict_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -14,11 +15,14 @@ dictController = APIRouter(dependencies=[Depends(get_current_user)]) @dictController.post("/dictType/get", response_model=DictTypePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) -async def get_system_dict_type_list(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): +async def get_system_dict_type_list(request: Request, dict_type_page_query: DictTypePageObject, query_db: Session = Depends(get_db)): try: - dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + # 获取全量数据 + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_page_query) + # 分页操作 + dict_type_page_query_result = get_page_obj(dict_type_query_result, dict_type_page_query.page_num, dict_type_page_query.page_size) logger.info('获取成功') - return response_200(data=dict_type_query_result, message="获取成功") + return response_200(data=dict_type_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -27,7 +31,7 @@ async def get_system_dict_type_list(request: Request, dict_type_query: DictTypeP @dictController.post("/dictType/all", dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) async def get_system_all_dict_type(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): try: - dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + dict_type_query_result = get_all_dict_type_services(query_db) logger.info('获取成功') return response_200(data=dict_type_query_result, message="获取成功") except Exception as e: @@ -100,11 +104,14 @@ async def query_detail_system_dict_type(request: Request, dict_id: int, query_db @dictController.post("/dictData/get", response_model=DictDataPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) -async def get_system_dict_data_list(request: Request, dict_data_query: DictDataPageObject, query_db: Session = Depends(get_db)): +async def get_system_dict_data_list(request: Request, dict_data_page_query: DictDataPageObject, query_db: Session = Depends(get_db)): try: - dict_data_query_result = get_dict_data_list(query_db, dict_data_query) + # 获取全量数据 + dict_data_query_result = get_dict_data_list_services(query_db, dict_data_page_query) + # 分页操作 + dict_data_page_query_result = get_page_obj(dict_data_query_result, dict_data_page_query.page_num, dict_data_page_query.page_size) logger.info('获取成功') - return response_200(data=dict_data_query_result, message="获取成功") + return response_200(data=dict_data_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index c5ef771..4813fdc 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -6,6 +6,7 @@ from module_admin.service.log_service import * from module_admin.entity.vo.log_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -14,11 +15,14 @@ logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user) @logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) -async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): +async def get_system_operation_log_list(request: Request, operation_log_page_query: OperLogPageObject, query_db: Session = Depends(get_db)): try: - operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) + # 获取全量数据 + operation_log_query_result = get_operation_log_list_services(query_db, operation_log_page_query) + # 分页操作 + operation_log_page_query_result = get_page_obj(operation_log_query_result, operation_log_page_query.page_num, operation_log_page_query.page_size) logger.info('获取成功') - return response_200(data=operation_log_query_result, message="获取成功") + return response_200(data=operation_log_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -68,11 +72,14 @@ async def query_detail_system_operation_log(request: Request, oper_id: int, quer @logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) -async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): +async def get_system_login_log_list(request: Request, login_log_page_query: LoginLogPageObject, query_db: Session = Depends(get_db)): try: - login_log_query_result = get_login_log_list_services(query_db, login_log_query) + # 获取全量数据 + login_log_query_result = get_login_log_list_services(query_db, login_log_page_query) + # 分页操作 + login_log_page_query_result = get_page_obj(login_log_query_result, login_log_page_query.page_num, login_log_page_query.page_size) logger.info('获取成功') - return response_200(data=login_log_query_result, message="获取成功") + return response_200(data=login_log_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 8703183..66adb6c 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -15,9 +15,10 @@ menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_tree_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_tree_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: @@ -26,9 +27,10 @@ async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, quer @menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: @@ -37,9 +39,10 @@ async def get_system_menu_tree_for_edit_option(request: Request, menu_query: Men @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_list(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_list_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_list_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index 740f9e8..2ed47d7 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -6,6 +6,7 @@ from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -25,11 +26,14 @@ async def get_system_post_select(request: Request, query_db: Session = Depends(g @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): +async def get_system_post_list(request: Request, post_page_query: PostPageObject, query_db: Session = Depends(get_db)): try: - post_query_result = get_post_list_services(query_db, post_query) + # 获取全量数据 + post_query_result = get_post_list_services(query_db, post_page_query) + # 分页操作 + post_page_query_result = get_page_obj(post_query_result, post_page_query.page_num, post_page_query.page_size) logger.info('获取成功') - return response_200(data=post_query_result, message="获取成功") + return response_200(data=post_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 562fff2..3af026b 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -6,6 +6,7 @@ from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -25,11 +26,13 @@ async def get_system_role_select(request: Request, query_db: Session = Depends(g @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): +async def get_system_role_list(request: Request, role_page_query: RolePageObject, query_db: Session = Depends(get_db)): try: - role_query_result = get_role_list_services(query_db, role_query) + role_query_result = get_role_list_services(query_db, role_page_query) + # 分页操作 + role_page_query_result = get_page_obj(role_query_result, role_page_query.page_num, role_page_query.page_size) logger.info('获取成功') - return response_200(data=role_query_result, message="获取成功") + return response_200(data=role_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index dd6cb63..70820fb 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -20,10 +20,8 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db)): try: - # 拆分user_query = 分页类 + UserModel - user_query = UserModel(**user_page_query.dict()) # 获取全量数据 - user_query_result = get_user_list_services(query_db, user_query) + user_query_result = get_user_list_services(query_db, user_page_query) # 分页操作 user_page_query_result = get_page_obj(user_query_result, user_page_query.page_num, user_page_query.page_size) logger.info('获取成功') @@ -90,7 +88,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.post("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) @@ -113,7 +111,6 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses # return response_500(data="", message="接口异常") - @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py index c15f36d..114afb7 100644 --- a/dash-fastapi-backend/module_admin/dao/dict_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -21,47 +21,25 @@ def get_all_dict_type(db: Session): return list_format_datetime(dict_type_info) -def get_dict_type_list(db: Session, page_object: DictTypePageObject): +def get_dict_type_list(db: Session, query_object: DictTypePageObject): """ 根据查询参数获取字典类型列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典类型列表信息对象 """ - count = db.query(SysDictType) \ - .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, - SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, - SysDictType.status == page_object.status if page_object.status else True, - SysDictType.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) dict_type_list = db.query(SysDictType) \ - .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, - SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, - SysDictType.status == page_object.status if page_object.status else True, + .filter(SysDictType.dict_name.like(f'%{query_object.dict_name}%') if query_object.dict_name else True, + SysDictType.dict_type.like(f'%{query_object.dict_type}%') if query_object.dict_type else True, + SysDictType.status == query_object.status if query_object.status else True, SysDictType.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(dict_type_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return DictTypePageObjectResponse(**result) + return list_format_datetime(dict_type_list) def add_dict_type_dao(db: Session, dict_type: DictTypeModel): @@ -121,41 +99,22 @@ def get_dict_data_detail_by_id(db: Session, dict_code: int): return dict_data_info -def get_dict_data_list(db: Session, page_object: DictDataPageObject): +def get_dict_data_list(db: Session, query_object: DictDataPageObject): """ 根据查询参数获取字典数据列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典数据列表信息对象 """ - count = db.query(SysDictData) \ - .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, - SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, - SysDictData.status == page_object.status if page_object.status else True - )\ - .order_by(SysDictData.dict_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) dict_data_list = db.query(SysDictData) \ - .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, - SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, - SysDictData.status == page_object.status if page_object.status else True + .filter(SysDictData.dict_type == query_object.dict_type if query_object.dict_type else True, + SysDictData.dict_label.like(f'%{query_object.dict_label}%') if query_object.dict_label else True, + SysDictData.status == query_object.status if query_object.status else True )\ .order_by(SysDictData.dict_sort)\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(dict_data_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return DictDataPageObjectResponse(**result) + return list_format_datetime(dict_data_list) def add_dict_data_dao(db: Session, dict_data: DictDataModel): diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py index 8447a15..780dc6c 100644 --- a/dash-fastapi-backend/module_admin/dao/log_dao.py +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -3,7 +3,6 @@ from module_admin.entity.do.log_do import SysOperLog, SysLogininfor from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse from utils.time_format_util import object_format_datetime, list_format_datetime -from utils.page_util import get_page_info from datetime import datetime, time @@ -15,49 +14,26 @@ def get_operation_log_detail_by_id(db: Session, oper_id: int): return object_format_datetime(operation_log_info) -def get_operation_log_list(db: Session, page_object: OperLogPageObject): +def get_operation_log_list(db: Session, query_object: OperLogPageObject): """ 根据查询参数获取操作日志列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 操作日志列表信息对象 """ - count = db.query(SysOperLog) \ - .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, - SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, - SysOperLog.business_type == page_object.business_type if page_object.business_type else True, - SysOperLog.status == page_object.status if page_object.status else True, - SysOperLog.oper_time.between( - datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.oper_time_start and page_object.oper_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) operation_log_list = db.query(SysOperLog) \ - .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, - SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, - SysOperLog.business_type == page_object.business_type if page_object.business_type else True, - SysOperLog.status == page_object.status if page_object.status else True, + .filter(SysOperLog.title.like(f'%{query_object.title}%') if query_object.title else True, + SysOperLog.oper_name.like(f'%{query_object.oper_name}%') if query_object.oper_name else True, + SysOperLog.business_type == query_object.business_type if query_object.business_type else True, + SysOperLog.status == query_object.status if query_object.status else True, SysOperLog.oper_time.between( - datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.oper_time_start and page_object.oper_time_end else True + datetime.combine(datetime.strptime(query_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.oper_time_start and query_object.oper_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(operation_log_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return OperLogPageObjectResponse(**result) + return list_format_datetime(operation_log_list) def add_operation_log_dao(db: Session, operation_log: OperLogModel): @@ -100,47 +76,25 @@ def clear_operation_log_dao(db: Session): db.commit() # 提交保存到数据库中 -def get_login_log_list(db: Session, page_object: LoginLogPageObject): +def get_login_log_list(db: Session, query_object: LoginLogPageObject): """ 根据查询参数获取登录日志列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 登录日志列表信息对象 """ - count = db.query(SysLogininfor) \ - .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, - SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysLogininfor.status == page_object.status if page_object.status else True, - SysLogininfor.login_time.between( - datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.login_time_start and page_object.login_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) login_log_list = db.query(SysLogininfor) \ - .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, - SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysLogininfor.status == page_object.status if page_object.status else True, + .filter(SysLogininfor.ipaddr.like(f'%{query_object.ipaddr}%') if query_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysLogininfor.status == query_object.status if query_object.status else True, SysLogininfor.login_time.between( - datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.login_time_start and page_object.login_time_end else True + datetime.combine(datetime.strptime(query_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.login_time_start and query_object.login_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(login_log_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return LoginLogPageObjectResponse(**result) + return list_format_datetime(login_log_list) def add_login_log_dao(db: Session, login_log: LogininforModel): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 63dbefa..7bc9bf4 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,5 +1,8 @@ +from sqlalchemy import and_ from sqlalchemy.orm import Session from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.do.user_do import SysUser, SysUserRole +from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse from utils.time_format_util import list_format_datetime @@ -12,42 +15,89 @@ def get_menu_detail_by_id(db: Session, menu_id: int): return menu_info -def get_menu_info_for_edit_option(db: Session, menu_info: MenuModel): - menu_result = db.query(SysMenu) \ - .filter(SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, - SysMenu.status == 0) \ - .all() +def get_menu_info_for_edit_option(db: Session, menu_info: MenuModel, user_id: int, role: list): + menu_result = [] + for item in role: + if item.role_id == 1: + menu_result = db.query(SysMenu) \ + .filter(SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, + SysMenu.status == 0) \ + .all() + else: + menu_result = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, + SysMenu.status == 0)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() return list_format_datetime(menu_result) -def get_menu_list_for_tree(db: Session, menu_info: MenuModel): - menu_query_all = db.query(SysMenu) \ - .filter(SysMenu.status == 0, - SysMenu.menu_name.like(f'%{menu_info.menu_name}%') if menu_info.menu_name else True) \ - .order_by(SysMenu.order_num) \ - .distinct().all() +def get_menu_list_for_tree(db: Session, menu_info: MenuModel, user_id: int, role: list): + menu_query_all = [] + for item in role: + if item.role_id == 1: + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == 0, + SysMenu.menu_name.like(f'%{menu_info.menu_name}%') if menu_info.menu_name else True) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + break + else: + menu_query_all = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.status == 0, + SysMenu.menu_name.like( + f'%{menu_info.menu_name}%') if menu_info.menu_name else True)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() return list_format_datetime(menu_query_all) -def get_menu_list(db: Session, page_object: MenuModel): +def get_menu_list(db: Session, page_object: MenuModel, user_id: int, role: list): """ 根据查询参数获取菜单列表信息 :param db: orm对象 :param page_object: 不分页查询参数对象 + :param user_id: 用户id + :param role: 用户角色列表 :return: 菜单列表信息对象 """ - if page_object.menu_name or page_object.status: - menu_query_all = db.query(SysMenu) \ - .filter(SysMenu.status == page_object.status if page_object.status else True, - SysMenu.menu_name.like(f'%{page_object.menu_name}%') if page_object.menu_name else True) \ - .order_by(SysMenu.order_num)\ - .distinct().all() - else: - menu_query_all = db.query(SysMenu) \ - .order_by(SysMenu.order_num) \ - .distinct().all() + menu_query_all = [] + for item in role: + if item.role_id == 1: + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like( + f'%{page_object.menu_name}%') if page_object.menu_name else True) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + break + else: + menu_query_all = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like( + f'%{page_object.menu_name}%') if page_object.menu_name else True)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() result = dict( rows=list_format_datetime(menu_query_all), diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index a9a70b1..4c222a0 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -2,7 +2,6 @@ from sqlalchemy.orm import Session from module_admin.entity.do.post_do import SysPost from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse from utils.time_format_util import list_format_datetime -from utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): @@ -30,41 +29,22 @@ def get_post_select_option_dao(db: Session): return post_info -def get_post_list(db: Session, page_object: PostPageObject): +def get_post_list(db: Session, query_object: PostPageObject): """ 根据查询参数获取岗位列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 岗位列表信息对象 """ - count = db.query(SysPost) \ - .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, - SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, - SysPost.status == page_object.status if page_object.status else True - )\ - .order_by(SysPost.post_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) post_list = db.query(SysPost) \ - .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, - SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, - SysPost.status == page_object.status if page_object.status else True + .filter(SysPost.post_code.like(f'%{query_object.post_code}%') if query_object.post_code else True, + SysPost.post_name.like(f'%{query_object.post_name}%') if query_object.post_name else True, + SysPost.status == query_object.status if query_object.status else True ) \ .order_by(SysPost.post_sort) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(post_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return PostPageObjectResponse(**result) + return list_format_datetime(post_list) def add_post_dao(db: Session, post: PostModel): diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 6f4f38b..2f75ea0 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -4,7 +4,6 @@ from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel from utils.time_format_util import list_format_datetime, object_format_datetime -from utils.page_util import get_page_info from datetime import datetime, time @@ -57,57 +56,33 @@ def get_role_detail_by_id(db: Session, role_id: int): def get_role_select_option_dao(db: Session): role_info = db.query(SysRole) \ - .filter(SysRole.status == 0, SysRole.del_flag == 0) \ + .filter(SysRole.role_id != 1, SysRole.status == 0, SysRole.del_flag == 0) \ .all() return role_info -def get_role_list(db: Session, page_object: RolePageObject): +def get_role_list(db: Session, query_object: RolePageObject): """ 根据查询参数获取角色列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 角色列表信息对象 """ - count = db.query(SysRole) \ - .filter(SysRole.del_flag == 0, - SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, - SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, - SysRole.status == page_object.status if page_object.status else True, - SysRole.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - )\ - .order_by(SysRole.role_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) role_list = db.query(SysRole) \ .filter(SysRole.del_flag == 0, - SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, - SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, - SysRole.status == page_object.status if page_object.status else True, + SysRole.role_name.like(f'%{query_object.role_name}%') if query_object.role_name else True, + SysRole.role_key.like(f'%{query_object.role_key}%') if query_object.role_key else True, + SysRole.status == query_object.status if query_object.status else True, SysRole.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True ) \ .order_by(SysRole.role_sort) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(role_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return RolePageObjectResponse(**result) + return list_format_datetime(role_list) def add_role_dao(db: Session, role: RoleModel): diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index f34330b..3174806 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -51,13 +51,21 @@ def get_user_by_id(db: Session, user_id: int): .outerjoin(SysUserPost, SysUser.user_id == SysUserPost.user_id) \ .outerjoin(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == 0)) \ .distinct().all() - query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ - .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ - .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ - .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ - .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ - .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ - .distinct().all() + query_user_menu_info = [] + for item in query_user_role_info: + if item.role_id == 1: + query_user_menu_info = db.query(SysMenu) \ + .filter(SysMenu.status == 0) \ + .distinct().all() + break + else: + query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ + .distinct().all() results = dict( user_basic_info=list_format_datetime(query_user_basic_info), user_dept_info=list_format_datetime(query_user_dept_info), @@ -192,31 +200,31 @@ def get_user_detail_by_id(db: Session, user_id: int): # return UserPageObjectResponse(**result) -def get_user_list(db: Session, user_object: UserModel): +def get_user_list(db: Session, query_object: UserPageObject): """ 根据查询参数获取用户列表信息 :param db: orm对象 - :param user_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 用户列表信息对象 """ user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, - SysUser.dept_id == user_object.dept_id if user_object.dept_id else True, - SysUser.user_name.like(f'%{user_object.user_name}%') if user_object.user_name else True, - SysUser.nick_name.like(f'%{user_object.nick_name}%') if user_object.nick_name else True, - SysUser.email.like(f'%{user_object.email}%') if user_object.email else True, - SysUser.phonenumber.like(f'%{user_object.phonenumber}%') if user_object.phonenumber else True, - SysUser.status == user_object.status if user_object.status else True, - SysUser.sex == user_object.sex if user_object.sex else True, + SysUser.dept_id == query_object.dept_id if query_object.dept_id else True, + SysUser.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysUser.nick_name.like(f'%{query_object.nick_name}%') if query_object.nick_name else True, + SysUser.email.like(f'%{query_object.email}%') if query_object.email else True, + SysUser.phonenumber.like(f'%{query_object.phonenumber}%') if query_object.phonenumber else True, + SysUser.status == query_object.status if query_object.status else True, + SysUser.sex == query_object.sex if query_object.sex else True, SysUser.create_time.between( - datetime.combine(datetime.strptime(user_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(user_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if user_object.create_time_start and user_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ .distinct().all() - result_list: List[Union[UserInfoJoinDept, None]] = [] + result_list: List[Union[dict, None]] = [] if user_list: for item in user_list: obj = dict( @@ -242,7 +250,7 @@ def get_user_list(db: Session, user_object: UserModel): ) result_list.append(obj) - return result_list + return format_datetime_dict_list(result_list) def add_user_dao(db: Session, user: UserModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index c492d8a..01cf656 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -32,8 +32,6 @@ class UserModel(BaseModel): update_by: Optional[str] update_time: Optional[str] remark: Optional[str] - create_time_start: Optional[str] - create_time_end: Optional[str] class Config: orm_mode = True @@ -158,33 +156,17 @@ class UserPageObject(UserModel): """ 用户管理分页查询模型 """ + create_time_start: Optional[str] + create_time_end: Optional[str] page_num: int page_size: int -class UserInfoJoinDept(BaseModel): +class UserInfoJoinDept(UserModel): """ 数据库查询用户列表返回模型 """ - user_id: Optional[int] - dept_id: Optional[int] dept_name: Optional[str] - user_name: Optional[str] - nick_name: Optional[str] - user_type: Optional[str] - email: Optional[str] - phonenumber: Optional[str] - sex: Optional[str] - avatar: Optional[str] - status: Optional[str] - del_flag: Optional[str] - login_ip: Optional[str] - login_date: Optional[str] - create_by: Optional[str] - create_time: Optional[str] - update_by: Optional[str] - update_time: Optional[str] - remark: Optional[str] class UserPageObjectResponse(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py index 9069456..bf4d943 100644 --- a/dash-fastapi-backend/module_admin/service/dict_service.py +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -2,17 +2,25 @@ from module_admin.entity.vo.dict_vo import * from module_admin.dao.dict_dao import * -def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObject): +def get_dict_type_list_services(result_db: Session, query_object: DictTypePageObject): """ 获取字典类型列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典类型列表信息对象 """ - if page_object.page_num and page_object.page_size: - dict_type_list_result = get_dict_type_list(result_db, page_object) - else: - dict_type_list_result = get_all_dict_type(result_db) + dict_type_list_result = get_dict_type_list(result_db, query_object) + + return dict_type_list_result + + +def get_all_dict_type_services(result_db: Session): + """ + 获取字所有典类型列表信息service + :param result_db: orm对象 + :return: 字典类型列表信息对象 + """ + dict_type_list_result = get_all_dict_type(result_db) return dict_type_list_result @@ -72,14 +80,14 @@ def detail_dict_type_services(result_db: Session, dict_id: int): return dict_type -def get_dict_data_list_services(result_db: Session, page_object: DictDataPageObject): +def get_dict_data_list_services(result_db: Session, query_object: DictDataPageObject): """ 获取字典数据列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典数据列表信息对象 """ - dict_data_list_result = get_dict_data_list(result_db, page_object) + dict_data_list_result = get_dict_data_list(result_db, query_object) return dict_data_list_result diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 6aac3d1..2944728 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -2,14 +2,14 @@ from module_admin.entity.vo.log_vo import * from module_admin.dao.log_dao import * -def get_operation_log_list_services(result_db: Session, page_object: OperLogPageObject): +def get_operation_log_list_services(result_db: Session, query_object: OperLogPageObject): """ 获取操作日志列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 操作日志列表信息对象 """ - operation_log_list_result = get_operation_log_list(result_db, page_object) + operation_log_list_result = get_operation_log_list(result_db, query_object) return operation_log_list_result @@ -72,14 +72,14 @@ def detail_operation_log_services(result_db: Session, oper_id: int): return operation_log -def get_login_log_list_services(result_db: Session, page_object: LoginLogPageObject): +def get_login_log_list_services(result_db: Session, query_object: LoginLogPageObject): """ 获取登录日志列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 登录日志列表信息对象 """ - operation_log_list_result = get_login_log_list(result_db, page_object) + operation_log_list_result = get_login_log_list(result_db, query_object) return operation_log_list_result diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index adebaa8..083e2e8 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -1,16 +1,18 @@ from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * +from module_admin.entity.vo.user_vo import CurrentUserInfoServiceResponse -def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): +def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单树信息service :param result_db: orm对象 :param page_object: 查询参数对象 + :param current_user: 当前用户对象 :return: 菜单树信息对象 """ menu_tree_option = [] - menu_list_result = get_menu_list_for_tree(result_db, MenuModel(**page_object.dict())) + menu_list_result = get_menu_list_for_tree(result_db, MenuModel(**page_object.dict()), current_user.user.user_id, current_user.role) menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) if page_object.type != 'role': menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) @@ -20,29 +22,31 @@ def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): return menu_tree_option -def get_menu_tree_for_edit_option_services(result_db: Session, page_object: MenuModel): +def get_menu_tree_for_edit_option_services(result_db: Session, page_object: MenuModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单编辑菜单树信息service :param result_db: orm对象 :param page_object: 查询参数对象 + :param current_user: 当前用户 :return: 菜单树信息对象 """ menu_tree_option = [] - menu_list_result = get_menu_info_for_edit_option(result_db, page_object) + menu_list_result = get_menu_info_for_edit_option(result_db, page_object, current_user.user.user_id, current_user.role) menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) return menu_tree_option -def get_menu_list_services(result_db: Session, page_object: MenuModel): +def get_menu_list_services(result_db: Session, page_object: MenuModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单列表信息service :param result_db: orm对象 :param page_object: 分页查询参数对象 + :param current_user: 当前用户对象 :return: 菜单列表信息对象 """ - menu_list_result = get_menu_list(result_db, page_object) + menu_list_result = get_menu_list(result_db, page_object, current_user.user.user_id, current_user.role) return menu_list_result diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index 507f477..5ecd66e 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -13,14 +13,14 @@ def get_post_select_option_services(result_db: Session): return post_list_result -def get_post_list_services(result_db: Session, page_object: PostPageObject): +def get_post_list_services(result_db: Session, query_object: PostPageObject): """ 获取岗位列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 岗位列表信息对象 """ - post_list_result = get_post_list(result_db, page_object) + post_list_result = get_post_list(result_db, query_object) return post_list_result diff --git a/dash-fastapi-backend/module_admin/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py index ed30123..50f4ec3 100644 --- a/dash-fastapi-backend/module_admin/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -13,14 +13,14 @@ def get_role_select_option_services(result_db: Session): return role_list_result -def get_role_list_services(result_db: Session, page_object: RolePageObject): +def get_role_list_services(result_db: Session, query_object: RolePageObject): """ 获取角色列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 角色列表信息对象 """ - role_list_result = get_role_list(result_db, page_object) + role_list_result = get_role_list(result_db, query_object) return role_list_result diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index b070411..5d8bb90 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -15,14 +15,14 @@ from module_admin.service.login_service import verify_password # return user_list_result -def get_user_list_services(result_db: Session, user_object: UserModel): +def get_user_list_services(result_db: Session, query_object: UserPageObject): """ 获取用户列表信息service :param result_db: orm对象 - :param user_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 用户列表信息对象 """ - user_list_result = get_user_list(result_db, user_object) + user_list_result = get_user_list(result_db, query_object) return user_list_result @@ -129,11 +129,15 @@ def reset_user_services(result_db: Session, page_object: ResetUserModel): :return: 重置用户校验结果 """ user = get_user_detail_by_id(result_db, user_id=page_object.user_id).user_basic_info[0] - if not verify_password(page_object.old_password, user.password): - result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + if page_object.old_password: + if not verify_password(page_object.old_password, user.password): + result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + else: + reset_user = page_object.dict(exclude_unset=True) + del reset_user['old_password'] + result = edit_user_dao(result_db, reset_user) else: reset_user = page_object.dict(exclude_unset=True) - del reset_user['old_password'] result = edit_user_dao(result_db, reset_user) return result diff --git a/dash-fastapi-backend/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py index 6b63ff8..dd78021 100644 --- a/dash-fastapi-backend/utils/page_util.py +++ b/dash-fastapi-backend/utils/page_util.py @@ -3,8 +3,6 @@ from typing import List from pydantic import BaseModel -from utils.time_format_util import format_datetime_dict_list - class PageModel(BaseModel): """ @@ -71,10 +69,10 @@ def get_page_obj(data_list: List, page_num: int, page_size: int): # 根据计算得到的起始索引和结束索引对数据列表进行切片 paginated_data = data_list[start:end] - has_next = True if math.ceil(len(data_list) / page_size) > page_num else False; + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False result = dict( - rows=format_datetime_dict_list(paginated_data), + rows=paginated_data, page_num=page_num, page_size=page_size, total=len(data_list), diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index ff353f4..e0454ee 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -90,17 +90,17 @@ def router(pathname, trigger): session['dept_info'] = current_user['dept'] session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] - valid_href_list = find_node_values(menu_info, 'href') - valid_href_list = valid_href_list + RouterConfig.STATIC_VALID_PATHNAME + dynamic_valid_pathname_list = find_node_values(menu_info, 'href') + valid_href_list = dynamic_valid_pathname_list + RouterConfig.STATIC_VALID_PATHNAME if pathname in valid_href_list: current_key = find_key_by_href(menu_info, pathname) + if pathname == '/': + current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' if trigger == 'load': # 根据pathname控制渲染行为 - if pathname == '/': - current_key = '首页' - if pathname == '/user/profile': - current_key = '个人资料' if pathname == '/login' or pathname == '/forget': # 重定向到主页面 return [ @@ -129,10 +129,6 @@ def router(pathname, trigger): # elif trigger == 'pushstate': else: - if pathname == '/': - current_key = '首页' - if pathname == '/user/profile': - current_key = '个人资料' return [ dash.no_update, None, diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index 028218e..3c06cba 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -49,7 +49,18 @@ def get_dept_table_data(search_click, operations, fold_click, dept_name, status_ item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) if item['parent_id'] == 0: - item['operation'] = [] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dept:edit' in button_perms else {}, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + } if 'system:dept:add' in button_perms else {}, + ] else: item['operation'] = [ { @@ -98,6 +109,7 @@ def reset_dept_query_params(reset_click): @app.callback( [Output('dept-modal', 'visible', allow_duplicate=True), Output('dept-modal', 'title'), + Output('dept-parent_id-div', 'hidden'), Output('dept-parent_id', 'treeData'), Output('dept-parent_id', 'value'), Output('dept-dept_name', 'value'), @@ -130,6 +142,7 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto return [ True, '新增部门', + False, tree_data, None, None, @@ -147,6 +160,7 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto return [ True, '新增部门', + False, tree_data, str(recently_button_clicked_row['key']), None, @@ -165,22 +179,42 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto dept_info_res = get_dept_detail_api(dept_id=dept_id) if dept_info_res['code'] == 200: dept_info = dept_info_res['data'] - return [ - True, - '编辑部门', - tree_data, - str(dept_info.get('parent_id')), - dept_info.get('dept_name'), - dept_info.get('order_num'), - dept_info.get('leader'), - dept_info.get('phone'), - dept_info.get('email'), - dept_info.get('status'), - {'timestamp': time.time()}, - None, - dept_info, - {'type': 'edit'} - ] + if dept_info.get('parent_id') == 0: + return [ + True, + '编辑部门', + True, + tree_data, + str(dept_info.get('parent_id')), + dept_info.get('dept_name'), + dept_info.get('order_num'), + dept_info.get('leader'), + dept_info.get('phone'), + dept_info.get('email'), + dept_info.get('status'), + {'timestamp': time.time()}, + None, + dept_info, + {'type': 'edit'} + ] + else: + return [ + True, + '编辑部门', + False, + tree_data, + str(dept_info.get('parent_id')), + dept_info.get('dept_name'), + dept_info.get('order_num'), + dept_info.get('leader'), + dept_info.get('phone'), + dept_info.get('email'), + dept_info.get('status'), + {'timestamp': time.time()}, + None, + dept_info, + {'type': 'edit'} + ] return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None, None] diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index fde15bd..5e2a33a 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -72,18 +72,21 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke else: item['status'] = dict(checked=False) item['key'] = str(item['role_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - } if 'system:role:edit' in button_perms else {}, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - } if 'system:role:remove' in button_perms else {}, - ] + if item['role_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:role:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:role:remove' in button_perms else {}, + ] return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py index e161a7a..18ac37d 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py @@ -11,7 +11,7 @@ from collections import OrderedDict from server import app from api.dept import get_dept_tree_api -from api.user import get_user_list_api, get_user_detail_api, add_user_api, edit_user_api, delete_user_api +from api.user import get_user_list_api, get_user_detail_api, add_user_api, edit_user_api, delete_user_api, reset_user_password_api from api.role import get_role_select_option_api from api.post import get_post_select_option_api @@ -100,20 +100,23 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio else: item['status'] = dict(checked=False) item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - } if 'system:user:edit' in button_perms else None, - { - 'title': '删除', - 'icon': 'antd-delete' - } if 'system:user:remove' in button_perms else None, - { - 'title': '重置密码', - 'icon': 'antd-key' - } if 'system:user:resetPwd' in button_perms else None - ] + if item['user_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + } if 'system:user:edit' in button_perms else None, + { + 'title': '删除', + 'icon': 'antd-delete' + } if 'system:user:remove' in button_perms else None, + { + 'title': '重置密码', + 'icon': 'antd-key' + } if 'system:user:resetPwd' in button_perms else None + ] return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] @@ -492,3 +495,59 @@ def user_delete_confirm(delete_confirm, user_ids_data): ] return [dash.no_update] * 3 + + +@app.callback( + [Output('user-reset-password-confirm-modal', 'visible'), + Output('reset-password-row-key-store', 'data'), + Output('reset-password-input', 'value')], + Input('user-list-table', 'nClicksDropdownItem'), + [State('user-list-table', 'recentlyClickedDropdownItemTitle'), + State('user-list-table', 'recentlyDropdownItemClickedRow')], + prevent_initial_call=True +) +def user_reset_password_modal(dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + if dropdown_click: + if recently_clicked_dropdown_item_title == '重置密码': + user_id = recently_dropdown_item_clicked_row['key'] + else: + return [dash.no_update] * 3 + + return [ + True, + {'user_id': user_id}, + None + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('user-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('user-reset-password-confirm-modal', 'okCounts'), + [State('reset-password-row-key-store', 'data'), + State('reset-password-input', 'value')], + prevent_initial_call=True +) +def user_reset_password_confirm(reset_confirm, user_id_data, reset_password): + if reset_confirm: + + user_id_data['password'] = reset_password + params = user_id_data + reset_button_info = reset_user_password_api(params) + if reset_button_info['code'] == 200: + return [ + {'type': 'reset-password'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('重置成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('重置失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index 3a22623..fe6a886 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -20,7 +20,18 @@ def render(button_perms): item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) if item['parent_id'] == 0: - item['operation'] = [] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dept:edit' in button_perms else {}, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + } if 'system:dept:add' in button_perms else {}, + ] else: item['operation'] = [ { @@ -250,25 +261,31 @@ def render(button_perms): fac.AntdRow( [ fac.AntdCol( - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-parent_id', - placeholder='请选择上级部门', - treeData=[], - treeNodeFilterProp='title', - style={ - 'width': '100%' - } - ), - label='上级部门', - required=True, - id='dept-parent_id-form-item', - labelCol={ - 'span': 4 - }, - wrapperCol={ - 'span': 20 - } + html.Div( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-parent_id', + placeholder='请选择上级部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': '100%' + } + ), + label='上级部门', + required=True, + id='dept-parent_id-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + ], + id='dept-parent_id-div', + hidden=False ), span=24 ), diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index e17ac94..5a8a6ee 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -24,18 +24,21 @@ def render(button_perms): else: item['status'] = dict(checked=False) item['key'] = str(item['role_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - } if 'system:role:edit' in button_perms else {}, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - } if 'system:role:remove' in button_perms else {}, - ] + if item['role_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:role:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:role:remove' in button_perms else {}, + ] return [ dcc.Store(id='role-button-perms-container', data=button_perms), diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 0911d78..59d7803 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -31,20 +31,23 @@ def render(button_perms): else: item['status'] = dict(checked=False) item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - } if 'system:user:edit' in button_perms else None, - { - 'title': '删除', - 'icon': 'antd-delete' - } if 'system:user:remove' in button_perms else None, - { - 'title': '重置密码', - 'icon': 'antd-key' - } if 'system:user:resetPwd' in button_perms else None - ] + if item['user_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + } if 'system:user:edit' in button_perms else None, + { + 'title': '删除', + 'icon': 'antd-delete' + } if 'system:user:remove' in button_perms else None, + { + 'title': '重置密码', + 'icon': 'antd-key' + } if 'system:user:resetPwd' in button_perms else None + ] return [ dcc.Store(id='user-button-perms-container', data=button_perms), @@ -836,20 +839,24 @@ def render(button_perms): # 重置密码modal fac.AntdModal( - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='reset-password-input', - mode='password' + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-password-input', + mode='password' + ), + label='请输入新密码' ), - ), - ], - layout='vertical' - ), + ], + layout='vertical' + ), + dcc.Store(id='reset-password-row-key-store') + ], id='user-reset-password-confirm-modal', visible=False, - title='提示', + title='重置密码', renderFooter=True, centered=True ), -- Gitee From 0093740cb5a14f0b5a5d912c887a168476860083 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 9 Aug 2023 16:18:41 +0800 Subject: [PATCH 15/54] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E5=85=AC=E5=91=8A=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88?= =?UTF-8?q?=E5=BC=95=E5=85=A5wangeditor=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../module_admin/annotation/log_annotation.py | 2 + .../controller/notice_controller.py | 92 + .../controller/user_controller.py | 1 + .../module_admin/dao/notice_dao.py | 83 + .../module_admin/entity/do/notice_do.py | 24 + .../module_admin/entity/vo/notice_vo.py | 57 + .../module_admin/service/notice_service.py | 69 + dash-fastapi-frontend/api/notice.py | 26 + .../assets/wangeditor/css/style.css | 27 + .../assets/wangeditor/index.js | 24129 ++++++++++++++++ .../callbacks/system_c/notice_c.py | 417 + dash-fastapi-frontend/store/store.py | 7 + .../views/system/notice/__init__.py | 472 +- 14 files changed, 25405 insertions(+), 3 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/controller/notice_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/notice_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/do/notice_do.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/notice_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/notice_service.py create mode 100644 dash-fastapi-frontend/api/notice.py create mode 100644 dash-fastapi-frontend/assets/wangeditor/css/style.css create mode 100644 dash-fastapi-frontend/assets/wangeditor/index.js create mode 100644 dash-fastapi-frontend/callbacks/system_c/notice_c.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 3cde6db..ee57bcb 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -13,6 +13,7 @@ from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from module_admin.controller.dict_controller import dictController +from module_admin.controller.notice_controller import noticeController from module_admin.controller.log_controller import logController from module_admin.controller.common_controller import commonController from config.env import RedisConfig @@ -82,6 +83,7 @@ app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) app.include_router(dictController, prefix="/system", tags=['system/dict']) +app.include_router(noticeController, prefix="/system", tags=['system/notice']) app.include_router(logController, prefix="/system", tags=['system/log']) app.include_router(commonController, prefix="/common", tags=['common']) diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index 5ef2342..4bafe6f 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -65,6 +65,8 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope finally: payload = await request.body() oper_param = json.dumps(json.loads(str(payload, 'utf-8')), ensure_ascii=False) + if len(oper_param) > 2000: + oper_param = '请求参数过长' # 调用原始函数 oper_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/dash-fastapi-backend/module_admin/controller/notice_controller.py b/dash-fastapi-backend/module_admin/controller/notice_controller.py new file mode 100644 index 0000000..a985268 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/notice_controller.py @@ -0,0 +1,92 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.notice_service import * +from module_admin.entity.vo.notice_vo import * +from utils.response_util import * +from utils.log_util import * +from utils.page_util import get_page_obj +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +noticeController = APIRouter(dependencies=[Depends(get_current_user)]) + + +@noticeController.post("/notice/get", response_model=NoticePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:list'))]) +async def get_system_notice_list(request: Request, notice_page_query: NoticePageObject, query_db: Session = Depends(get_db)): + try: + # 获取全量数据 + notice_query_result = get_notice_list_services(query_db, notice_page_query) + # 分页操作 + notice_page_query_result = get_page_obj(notice_query_result, notice_page_query.page_num, notice_page_query.page_size) + logger.info('获取成功') + return response_200(data=notice_page_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.post("/notice/add", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:add'))]) +@log_decorator(title='通知公告管理', business_type=1) +async def add_system_notice(request: Request, add_notice: NoticeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_notice.create_by = current_user.user.user_name + add_notice.update_by = current_user.user.user_name + add_notice_result = add_notice_services(query_db, add_notice) + logger.info(add_notice_result.message) + if add_notice_result.is_success: + return response_200(data=add_notice_result, message=add_notice_result.message) + else: + return response_400(data="", message=add_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.patch("/notice/edit", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:edit'))]) +@log_decorator(title='通知公告管理', business_type=2) +async def edit_system_notice(request: Request, edit_notice: NoticeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_notice.update_by = current_user.user.user_name + edit_notice.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_notice_result = edit_notice_services(query_db, edit_notice) + if edit_notice_result.is_success: + logger.info(edit_notice_result.message) + return response_200(data=edit_notice_result, message=edit_notice_result.message) + else: + logger.warning(edit_notice_result.message) + return response_400(data="", message=edit_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.post("/notice/delete", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:remove'))]) +@log_decorator(title='通知公告管理', business_type=3) +async def delete_system_notice(request: Request, delete_notice: DeleteNoticeModel, query_db: Session = Depends(get_db)): + try: + delete_notice_result = delete_notice_services(query_db, delete_notice) + if delete_notice_result.is_success: + logger.info(delete_notice_result.message) + return response_200(data=delete_notice_result, message=delete_notice_result.message) + else: + logger.warning(delete_notice_result.message) + return response_400(data="", message=delete_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.get("/notice/{notice_id}", response_model=NoticeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:edit'))]) +async def query_detail_system_post(request: Request, notice_id: int, query_db: Session = Depends(get_db)): + try: + detail_notice_result = detail_notice_services(query_db, notice_id) + logger.info(f'获取notice_id为{notice_id}的信息成功') + return response_200(data=detail_notice_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 70820fb..f51a8b6 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -112,6 +112,7 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/module_admin/dao/notice_dao.py b/dash-fastapi-backend/module_admin/dao/notice_dao.py new file mode 100644 index 0000000..3b2bf56 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/notice_dao.py @@ -0,0 +1,83 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.notice_do import SysNotice +from module_admin.entity.vo.notice_vo import NoticeModel, NoticePageObject, NoticePageObjectResponse, CrudNoticeResponse +from utils.time_format_util import list_format_datetime, object_format_datetime +from datetime import datetime, time + + +def get_notice_detail_by_id(db: Session, notice_id: int): + notice_info = db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice_id) \ + .first() + + return object_format_datetime(notice_info) + + +def get_notice_list(db: Session, query_object: NoticePageObject): + """ + 根据查询参数获取通知公告列表信息 + :param db: orm对象 + :param query_object: 查询参数对象 + :return: 通知公告列表信息对象 + """ + notice_list = db.query(SysNotice) \ + .filter(SysNotice.notice_title.like(f'%{query_object.notice_title}%') if query_object.notice_title else True, + SysNotice.update_by.like(f'%{query_object.update_by}%') if query_object.update_by else True, + SysNotice.notice_type == query_object.notice_type if query_object.notice_type else True, + SysNotice.create_time.between( + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True + ) \ + .distinct().all() + + return list_format_datetime(notice_list) + + +def add_notice_dao(db: Session, notice: NoticeModel): + """ + 新增通知公告数据库操作 + :param db: orm对象 + :param notice: 通知公告对象 + :return: 新增校验结果 + """ + db_notice = SysNotice(**notice.dict()) + db.add(db_notice) + db.commit() # 提交保存到数据库中 + db.refresh(db_notice) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudNoticeResponse(**result) + + +def edit_notice_dao(db: Session, notice: dict): + """ + 编辑通知公告数据库操作 + :param db: orm对象 + :param notice: 需要更新的通知公告字典 + :return: 编辑校验结果 + """ + is_notice_id = db.query(SysNotice).filter(SysNotice.notice_id == notice.get('notice_id')).all() + if not is_notice_id: + result = dict(is_success=False, message='通知公告不存在') + else: + db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice.get('notice_id')) \ + .update(notice) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudNoticeResponse(**result) + + +def delete_notice_dao(db: Session, notice: NoticeModel): + """ + 删除通知公告数据库操作 + :param db: orm对象 + :param notice: 通知公告对象 + :return: + """ + db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice.notice_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/do/notice_do.py b/dash-fastapi-backend/module_admin/entity/do/notice_do.py new file mode 100644 index 0000000..94e1f22 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/do/notice_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, DateTime, LargeBinary +from config.database import Base, engine +from datetime import datetime + + +class SysNotice(Base): + """ + 通知公告表 + """ + __tablename__ = 'sys_notice' + + notice_id = Column(Integer, primary_key=True, autoincrement=True, comment='公告ID') + notice_title = Column(String(50, collation='utf8_general_ci'), nullable=False, comment='公告标题') + notice_type = Column(String(1, collation='utf8_general_ci'), nullable=False, comment='公告类型(1通知 2公告)') + notice_content = Column(LargeBinary, comment='公告内容') + status = Column(String(1, collation='utf8_general_ci'), default='0', comment='公告状态(0正常 1关闭)') + create_by = Column(String(64, collation='utf8_general_ci'), default='', comment='创建者') + create_time = Column(DateTime, comment='创建时间', default=datetime.now()) + update_by = Column(String(64, collation='utf8_general_ci'), default='', comment='更新者') + update_time = Column(DateTime, comment='更新时间', default=datetime.now()) + remark = Column(String(255, collation='utf8_general_ci'), comment='备注') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py b/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py new file mode 100644 index 0000000..57d6753 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class NoticeModel(BaseModel): + """ + 通知公告表对应pydantic模型 + """ + notice_id: Optional[int] + notice_title: Optional[str] + notice_type: Optional[str] + notice_content: Optional[bytes] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class NoticePageObject(NoticeModel): + """ + 通知公告管理分页查询模型 + """ + create_time_start: Optional[str] + create_time_end: Optional[str] + page_num: int + page_size: int + + +class NoticePageObjectResponse(BaseModel): + """ + 通知公告管理列表分页查询返回模型 + """ + rows: List[Union[NoticeModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class CrudNoticeResponse(BaseModel): + """ + 操作通知公告响应模型 + """ + is_success: bool + message: str + + +class DeleteNoticeModel(BaseModel): + """ + 删除通知公告模型 + """ + notice_ids: str diff --git a/dash-fastapi-backend/module_admin/service/notice_service.py b/dash-fastapi-backend/module_admin/service/notice_service.py new file mode 100644 index 0000000..ee03af6 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/notice_service.py @@ -0,0 +1,69 @@ +from module_admin.entity.vo.notice_vo import * +from module_admin.dao.notice_dao import * + + +def get_notice_list_services(result_db: Session, query_object: NoticePageObject): + """ + 获取通知公告列表信息service + :param result_db: orm对象 + :param query_object: 查询参数对象 + :return: 通知公告列表信息对象 + """ + notice_list_result = get_notice_list(result_db, query_object) + + return notice_list_result + + +def add_notice_services(result_db: Session, page_object: NoticeModel): + """ + 新增通知公告信息service + :param result_db: orm对象 + :param page_object: 新增通知公告对象 + :return: 新增通知公告校验结果 + """ + add_notice_result = add_notice_dao(result_db, page_object) + + return add_notice_result + + +def edit_notice_services(result_db: Session, page_object: NoticeModel): + """ + 编辑通知公告信息service + :param result_db: orm对象 + :param page_object: 编辑通知公告对象 + :return: 编辑通知公告校验结果 + """ + edit_notice = page_object.dict(exclude_unset=True) + edit_notice_result = edit_notice_dao(result_db, edit_notice) + + return edit_notice_result + + +def delete_notice_services(result_db: Session, page_object: DeleteNoticeModel): + """ + 删除通知公告信息service + :param result_db: orm对象 + :param page_object: 删除通知公告对象 + :return: 删除通知公告校验结果 + """ + if page_object.notice_ids.split(','): + notice_id_list = page_object.notice_ids.split(',') + for notice_id in notice_id_list: + notice_id_dict = dict(notice_id=notice_id) + delete_notice_dao(result_db, NoticeModel(**notice_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入岗位id为空') + return CrudNoticeResponse(**result) + + +def detail_notice_services(result_db: Session, notice_id: int): + """ + 获取通知公告详细信息service + :param result_db: orm对象 + :param notice_id: 通知公告id + :return: 通知公告id对应的信息 + """ + notice = get_notice_detail_by_id(result_db, notice_id=notice_id) + + return notice diff --git a/dash-fastapi-frontend/api/notice.py b/dash-fastapi-frontend/api/notice.py new file mode 100644 index 0000000..af972ed --- /dev/null +++ b/dash-fastapi-frontend/api/notice.py @@ -0,0 +1,26 @@ +from utils.request import api_request + + +def get_notice_list_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/get', is_headers=True, json=page_obj) + + +def add_notice_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/add', is_headers=True, json=page_obj) + + +def edit_notice_api(page_obj: dict): + + return api_request(method='patch', url='/system/notice/edit', is_headers=True, json=page_obj) + + +def delete_notice_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/delete', is_headers=True, json=page_obj) + + +def get_notice_detail_api(notice_id: int): + + return api_request(method='get', url=f'/system/notice/{notice_id}', is_headers=True) diff --git a/dash-fastapi-frontend/assets/wangeditor/css/style.css b/dash-fastapi-frontend/assets/wangeditor/css/style.css new file mode 100644 index 0000000..6d13c91 --- /dev/null +++ b/dash-fastapi-frontend/assets/wangeditor/css/style.css @@ -0,0 +1,27 @@ +:root, +:host { + --w-e-textarea-bg-color: #fff; + --w-e-textarea-color: #333; + --w-e-textarea-border-color: #ccc; + --w-e-textarea-slight-border-color: #e8e8e8; + --w-e-textarea-slight-color: #d4d4d4; + --w-e-textarea-slight-bg-color: #f5f2f0; + --w-e-textarea-selected-border-color: #B4D5FF; + --w-e-textarea-handler-bg-color: #4290f7; + --w-e-toolbar-color: #595959; + --w-e-toolbar-bg-color: #fff; + --w-e-toolbar-active-color: #333; + --w-e-toolbar-active-bg-color: #f1f1f1; + --w-e-toolbar-disabled-color: #999; + --w-e-toolbar-border-color: #e8e8e8; + --w-e-modal-button-bg-color: #fafafa; + --w-e-modal-button-border-color: #d9d9d9; +} + +.w-e-text-container *,.w-e-toolbar *{box-sizing:border-box;margin:0;outline:none;padding:0}.w-e-text-container blockquote,.w-e-text-container li,.w-e-text-container p,.w-e-text-container td,.w-e-text-container th,.w-e-toolbar *{line-height:1.5}.w-e-text-container{background-color:var(--w-e-textarea-bg-color);color:var(--w-e-textarea-color);height:100%;position:relative}.w-e-text-container .w-e-scroll{-webkit-overflow-scrolling:touch;height:100%}.w-e-text-container [data-slate-editor]{word-wrap:break-word;border-top:1px solid transparent;min-height:100%;outline:0;padding:0 10px;white-space:pre-wrap}.w-e-text-container [data-slate-editor] p{margin:15px 0}.w-e-text-container [data-slate-editor] h1,.w-e-text-container [data-slate-editor] h2,.w-e-text-container [data-slate-editor] h3,.w-e-text-container [data-slate-editor] h4,.w-e-text-container [data-slate-editor] h5{margin:20px 0}.w-e-text-container [data-slate-editor] img{cursor:default;display:inline!important;max-width:100%;min-height:20px;min-width:20px}.w-e-text-container [data-slate-editor] span{text-indent:0}.w-e-text-container [data-slate-editor] [data-selected=true]{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-placeholder{font-style:italic;left:10px;top:17px;width:90%}.w-e-max-length-info,.w-e-text-placeholder{color:var(--w-e-textarea-slight-color);pointer-events:none;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none}.w-e-max-length-info{bottom:.5em;right:1em}.w-e-bar{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-color);font-size:14px;padding:0 5px}.w-e-bar svg{fill:var(--w-e-toolbar-color);height:14px;width:14px}.w-e-bar-show{display:flex}.w-e-bar-hidden{display:none}.w-e-hover-bar{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 5px #0000001f;position:absolute}.w-e-toolbar{flex-wrap:wrap;position:relative}.w-e-bar-divider{background-color:var(--w-e-toolbar-border-color);display:inline-flex;height:40px;margin:0 5px;width:1px}.w-e-bar-item{display:flex;height:40px;padding:4px;position:relative;text-align:center}.w-e-bar-item,.w-e-bar-item button{align-items:center;justify-content:center}.w-e-bar-item button{background:transparent;border:none;color:var(--w-e-toolbar-color);cursor:pointer;display:inline-flex;height:32px;overflow:hidden;padding:0 8px;white-space:nowrap}.w-e-bar-item button:hover{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item button .title{margin-left:5px}.w-e-bar-item .active{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item .disabled{color:var(--w-e-toolbar-disabled-color);cursor:not-allowed}.w-e-bar-item .disabled svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-menu-tooltip-v5:before{background-color:var(--w-e-toolbar-active-color);border-radius:5px;color:var(--w-e-toolbar-bg-color);content:attr(data-tooltip);font-size:.75em;opacity:0;padding:5px 10px;position:absolute;text-align:center;top:40px;transition:opacity .6s;visibility:hidden;white-space:pre;z-index:1}.w-e-menu-tooltip-v5:after{border:5px solid transparent;border-bottom:5px solid var(--w-e-toolbar-active-color);content:"";opacity:0;position:absolute;top:30px;transition:opacity .6s;visibility:hidden}.w-e-menu-tooltip-v5:hover:after,.w-e-menu-tooltip-v5:hover:before{opacity:1;visibility:visible}.w-e-menu-tooltip-v5.tooltip-right:before{left:100%;top:10px}.w-e-menu-tooltip-v5.tooltip-right:after{border-bottom-color:transparent;border-left-color:transparent;border-right-color:var(--w-e-toolbar-active-color);border-top-color:transparent;left:100%;margin-left:-10px;top:16px}.w-e-bar-item-group .w-e-bar-item-menus-container{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;display:none;left:0;margin-top:40px;position:absolute;top:0;z-index:1}.w-e-bar-item-group:hover .w-e-bar-item-menus-container{display:block}.w-e-select-list{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;left:0;margin-top:40px;max-height:350px;min-width:100px;overflow-y:auto;position:absolute;top:0;z-index:1}.w-e-select-list ul{line-height:1;list-style:none}.w-e-select-list ul .selected{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li{cursor:pointer;padding:7px 0 7px 25px;position:relative;text-align:left;white-space:nowrap}.w-e-select-list ul li:hover{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li svg{left:0;margin-left:5px;margin-top:-7px;position:absolute;top:50%}.w-e-bar-bottom .w-e-select-list{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-drop-panel{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;margin-top:40px;min-width:200px;padding:10px;position:absolute;top:0;z-index:1}.w-e-bar-bottom .w-e-drop-panel{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-modal{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;color:var(--w-e-toolbar-color);font-size:14px;min-height:40px;min-width:100px;padding:20px 15px 0;position:absolute;text-align:left;z-index:1}.w-e-modal .btn-close{cursor:pointer;line-height:1;padding:5px;position:absolute;right:8px;top:7px}.w-e-modal .btn-close svg{fill:var(--w-e-toolbar-color);height:10px;width:10px}.w-e-modal .babel-container{display:block;margin-bottom:15px}.w-e-modal .babel-container span{display:block;margin-bottom:10px}.w-e-modal .button-container{margin-bottom:15px}.w-e-modal button{background-color:var(--w-e-modal-button-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);cursor:pointer;font-weight:400;height:32px;padding:4.5px 15px;text-align:center;touch-action:manipulation;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{font-feature-settings:"tnum";background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);font-variant:tabular-nums;padding:4.5px 11px;transition:all .3s;width:100%}.w-e-modal textarea{min-height:60px}body .w-e-modal,body .w-e-modal *{box-sizing:border-box}.w-e-progress-bar{background-color:var(--w-e-textarea-handler-bg-color);height:1px;position:absolute;transition:width .3s;width:0}.w-e-full-screen-container{bottom:0!important;display:flex!important;flex-direction:column!important;height:100%!important;left:0!important;margin:0!important;padding:0!important;position:fixed;right:0!important;top:0!important;width:100%!important}.w-e-full-screen-container [data-w-e-textarea=true]{flex:1!important} +.w-e-text-container [data-slate-editor] code{background-color:var(--w-e-textarea-slight-bg-color);border-radius:3px;font-family:monospace;padding:3px}.w-e-panel-content-color{list-style:none;text-align:left;width:230px}.w-e-panel-content-color li{border:1px solid var(--w-e-toolbar-bg-color);border-radius:3px 3px;cursor:pointer;display:inline-block;padding:2px}.w-e-panel-content-color li:hover{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color li .color-block{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px 3px;height:17px;width:17px}.w-e-panel-content-color .active{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color .clear{line-height:1.5;margin-bottom:5px;width:100%}.w-e-panel-content-color .clear svg{height:16px;margin-bottom:-4px;width:16px}.w-e-text-container [data-slate-editor] blockquote{background-color:var(--w-e-textarea-slight-bg-color);border-left:8px solid var(--w-e-textarea-selected-border-color);display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px}.w-e-panel-content-emotion{font-size:20px;list-style:none;text-align:left;width:300px}.w-e-panel-content-emotion li{border-radius:3px 3px;cursor:pointer;display:inline-block;padding:0 5px}.w-e-panel-content-emotion li:hover{background-color:var(--w-e-textarea-slight-bg-color)}.w-e-textarea-divider{border-radius:3px;margin:20px auto;padding:20px}.w-e-textarea-divider hr{background-color:var(--w-e-textarea-border-color);border:0;display:block;height:1px}.w-e-text-container [data-slate-editor] pre>code{background-color:var(--w-e-textarea-slight-bg-color);border:1px solid var(--w-e-textarea-slight-border-color);border-radius:4px 4px;display:block;font-size:14px;padding:10px;text-indent:0}.w-e-text-container [data-slate-editor] .w-e-image-container{display:inline-block;margin:0 3px}.w-e-text-container [data-slate-editor] .w-e-image-container:hover{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-container [data-slate-editor] .w-e-selected-image-container{overflow:hidden;position:relative}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .w-e-image-dragger{background-color:var(--w-e-textarea-handler-bg-color);height:7px;position:absolute;width:7px}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-top{cursor:nwse-resize;left:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-top{cursor:nesw-resize;right:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-bottom{bottom:0;cursor:nesw-resize;left:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-bottom{bottom:0;cursor:nwse-resize;right:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container:hover{box-shadow:none}.w-e-text-container [contenteditable=false] .w-e-image-container:hover{box-shadow:none} + +.w-e-text-container [data-slate-editor] .table-container{border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin-top:10px;overflow-x:auto;padding:10px;width:100%}.w-e-text-container [data-slate-editor] table{border-collapse:collapse}.w-e-text-container [data-slate-editor] table td,.w-e-text-container [data-slate-editor] table th{border:1px solid var(--w-e-textarea-border-color);line-height:1.5;min-width:30px;padding:3px 5px;text-align:left}.w-e-text-container [data-slate-editor] table th{background-color:var(--w-e-textarea-slight-bg-color);font-weight:700;text-align:center}.w-e-panel-content-table{background-color:var(--w-e-toolbar-bg-color)}.w-e-panel-content-table table{border-collapse:collapse}.w-e-panel-content-table td{border:1px solid var(--w-e-toolbar-border-color);cursor:pointer;height:15px;padding:3px 5px;width:20px}.w-e-panel-content-table td.active{background-color:var(--w-e-toolbar-active-bg-color)} +.w-e-textarea-video-container{background-image:linear-gradient(45deg,#eee 25%,transparent 0,transparent 75%,#eee 0,#eee),linear-gradient(45deg,#eee 25%,#fff 0,#fff 75%,#eee 0,#eee);background-position:0 0,10px 10px;background-size:20px 20px;border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin:10px auto 0;padding:10px 0;text-align:center} + +.w-e-text-container [data-slate-editor] pre>code{word-wrap:normal;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;-webkit-hyphens:none;hyphens:none;line-height:1.5;margin:.5em 0;overflow:auto;padding:1em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px #fff;white-space:pre;word-break:normal;word-spacing:normal}.w-e-text-container [data-slate-editor] pre>code .token.cdata,.w-e-text-container [data-slate-editor] pre>code .token.comment,.w-e-text-container [data-slate-editor] pre>code .token.doctype,.w-e-text-container [data-slate-editor] pre>code .token.prolog{color:#708090}.w-e-text-container [data-slate-editor] pre>code .token.punctuation{color:#999}.w-e-text-container [data-slate-editor] pre>code .token.namespace{opacity:.7}.w-e-text-container [data-slate-editor] pre>code .token.boolean,.w-e-text-container [data-slate-editor] pre>code .token.constant,.w-e-text-container [data-slate-editor] pre>code .token.deleted,.w-e-text-container [data-slate-editor] pre>code .token.number,.w-e-text-container [data-slate-editor] pre>code .token.property,.w-e-text-container [data-slate-editor] pre>code .token.symbol,.w-e-text-container [data-slate-editor] pre>code .token.tag{color:#905}.w-e-text-container [data-slate-editor] pre>code .token.attr-name,.w-e-text-container [data-slate-editor] pre>code .token.builtin,.w-e-text-container [data-slate-editor] pre>code .token.char,.w-e-text-container [data-slate-editor] pre>code .token.inserted,.w-e-text-container [data-slate-editor] pre>code .token.selector,.w-e-text-container [data-slate-editor] pre>code .token.string{color:#690}.w-e-text-container [data-slate-editor] pre>code .language-css .token.string,.w-e-text-container [data-slate-editor] pre>code .style .token.string,.w-e-text-container [data-slate-editor] pre>code .token.entity,.w-e-text-container [data-slate-editor] pre>code .token.operator,.w-e-text-container [data-slate-editor] pre>code .token.url{color:#9a6e3a}.w-e-text-container [data-slate-editor] pre>code .token.atrule,.w-e-text-container [data-slate-editor] pre>code .token.attr-value,.w-e-text-container [data-slate-editor] pre>code .token.keyword{color:#07a}.w-e-text-container [data-slate-editor] pre>code .token.class-name,.w-e-text-container [data-slate-editor] pre>code .token.function{color:#dd4a68}.w-e-text-container [data-slate-editor] pre>code .token.important,.w-e-text-container [data-slate-editor] pre>code .token.regex,.w-e-text-container [data-slate-editor] pre>code .token.variable{color:#e90}.w-e-text-container [data-slate-editor] pre>code .token.bold,.w-e-text-container [data-slate-editor] pre>code .token.important{font-weight:700}.w-e-text-container [data-slate-editor] pre>code .token.italic{font-style:italic}.w-e-text-container [data-slate-editor] pre>code .token.entity{cursor:help} \ No newline at end of file diff --git a/dash-fastapi-frontend/assets/wangeditor/index.js b/dash-fastapi-frontend/assets/wangeditor/index.js new file mode 100644 index 0000000..a2233e4 --- /dev/null +++ b/dash-fastapi-frontend/assets/wangeditor/index.js @@ -0,0 +1,24129 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.wangEditor = {})); +})(this, (function (exports) { 'use strict'; + + /** + * @description browser polyfill + * @author wangfupeng + */ + var _a; + // @ts-nocheck + // 必须是浏览器环境 + if (typeof global === 'undefined') { + // 检查 IE 浏览器 + if ('ActiveXObject' in window) { + var info = '抱歉,wangEditor V5+ 版本开始,不在支持 IE 浏览器'; + info += '\n Sorry, wangEditor V5+ versions do not support IE browser.'; + console.error(info); + } + globalThisPolyfill(); + AggregateErrorPolyfill(); + } + else if (global && ((_a = global.navigator) === null || _a === void 0 ? void 0 : _a.userAgent.match('QQBrowser'))) { + // 兼容 QQ 浏览器 AggregateError 报错 + globalThisPolyfill(); + AggregateErrorPolyfill(); + } + function globalThisPolyfill() { + // 部分浏览器不支持 globalThis + if (typeof globalThis === 'undefined') { + // @ts-ignore + window.globalThis = window; + } + } + function AggregateErrorPolyfill() { + if (typeof AggregateError === 'undefined') { + window.AggregateError = function (errors, msg) { + var err = new Error(msg); + err.errors = errors; + return err; + }; + } + } + + /** + * @description node polyfill + * @author wangfupeng + */ + // @ts-nocheck + // 必须是 node 环境 + if (typeof global === 'object') { + // 用于 nodejs ,避免报错 + var globalProperty = Object.getOwnPropertyDescriptor(global, 'window'); + // global.window 为空则直接写入 + // 部分框架下已经定义了global.window且是不可写属性 + if (!global.window || globalProperty.set) { + global.window = global; + global.requestAnimationFrame = function () { }; + global.navigator = { + userAgent: '', + }; + global.location = { + hostname: '0.0.0.0', + port: 0, + protocol: 'http:', + }; + global.btoa = function () { }; + global.crypto = { + getRandomValues: function (buffer) { + return nodeCrypto.randomFillSync(buffer); + }, + }; + } + if (global.document != null) { + // SSR 环境下可能会报错 (issue 4409) + if (global.document.getElementsByTagName == null) { + global.document.getElementsByTagName = function () { return []; }; + } + } + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule$1(fn) { + var module = { exports: {} }; + return fn(module, module.exports), module.exports; + } + + /*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + + function isObject$4(o) { + return Object.prototype.toString.call(o) === '[object Object]'; + } + + function isPlainObject$2(o) { + var ctor,prot; + + if (isObject$4(o) === false) return false; + + // If has modified constructor + ctor = o.constructor; + if (ctor === undefined) return true; + + // If has modified prototype + prot = ctor.prototype; + if (isObject$4(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; + } + + var isPlainObject_2 = isPlainObject$2; + + var isPlainObject_1 = /*#__PURE__*/Object.defineProperty({ + isPlainObject: isPlainObject_2 + }, '__esModule', {value: true}); + + var _ref; + + // Should be no imports here! + // Some things that should be evaluated before all else... + // We only want to know if non-polyfilled symbols are available + var hasSymbol = typeof Symbol !== "undefined" && typeof + /*#__PURE__*/ + Symbol("x") === "symbol"; + var hasMap = typeof Map !== "undefined"; + var hasSet = typeof Set !== "undefined"; + var hasProxies = typeof Proxy !== "undefined" && typeof Proxy.revocable !== "undefined" && typeof Reflect !== "undefined"; + /** + * The sentinel value returned by producers to replace the draft with undefined. + */ + + var NOTHING = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-nothing") : (_ref = {}, _ref["immer-nothing"] = true, _ref); + /** + * To let Immer treat your class instances as plain immutable objects + * (albeit with a custom prototype), you must define either an instance property + * or a static property on each of your custom classes. + * + * Otherwise, your class instance will never be drafted, which means it won't be + * safe to mutate in a produce callback. + */ + + var DRAFTABLE = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-draftable") : "__$immer_draftable"; + var DRAFT_STATE = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-state") : "__$immer_state"; // Even a polyfilled Symbol might provide Symbol.iterator + + var iteratorSymbol$1 = typeof Symbol != "undefined" && Symbol.iterator || "@@iterator"; + + var errors = { + 0: "Illegal state", + 1: "Immer drafts cannot have computed properties", + 2: "This object has been frozen and should not be mutated", + 3: function _(data) { + return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data; + }, + 4: "An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.", + 5: "Immer forbids circular references", + 6: "The first or second argument to `produce` must be a function", + 7: "The third argument to `produce` must be a function or undefined", + 8: "First argument to `createDraft` must be a plain object, an array, or an immerable object", + 9: "First argument to `finishDraft` must be a draft returned by `createDraft`", + 10: "The given draft is already finalized", + 11: "Object.defineProperty() cannot be used on an Immer draft", + 12: "Object.setPrototypeOf() cannot be used on an Immer draft", + 13: "Immer only supports deleting array indices", + 14: "Immer only supports setting array indices and the 'length' property", + 15: function _(path) { + return "Cannot apply patch, path doesn't resolve: " + path; + }, + 16: 'Sets cannot have "replace" patches.', + 17: function _(op) { + return "Unsupported patch operation: " + op; + }, + 18: function _(plugin) { + return "The plugin for '" + plugin + "' has not been loaded into Immer. To enable the plugin, import and call `enable" + plugin + "()` when initializing your application."; + }, + 20: "Cannot use proxies if Proxy, Proxy.revocable or Reflect are not available", + 21: function _(thing) { + return "produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '" + thing + "'"; + }, + 22: function _(thing) { + return "'current' expects a draft, got: " + thing; + }, + 23: function _(thing) { + return "'original' expects a draft, got: " + thing; + }, + 24: "Patching reserved attributes like __proto__, prototype and constructor is not allowed" + }; + function die(error) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + { + var e = errors[error]; + var msg = !e ? "unknown error nr: " + error : typeof e === "function" ? e.apply(null, args) : e; + throw new Error("[Immer] " + msg); + } + } + + /** Returns true if the given value is an Immer draft */ + + + + function isDraft(value) { + return !!value && !!value[DRAFT_STATE]; + } + /** Returns true if the given value can be drafted by Immer */ + + + + function isDraftable(value) { + if (!value) return false; + return isPlainObject$1(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!value.constructor[DRAFTABLE] || isMap(value) || isSet(value); + } + var objectCtorString = + /*#__PURE__*/ + Object.prototype.constructor.toString(); + + + function isPlainObject$1(value) { + if (!value || typeof value !== "object") return false; + var proto = Object.getPrototypeOf(value); + + if (proto === null) { + return true; + } + + var Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor; + if (Ctor === Object) return true; + return typeof Ctor == "function" && Function.toString.call(Ctor) === objectCtorString; + } + function original(value) { + if (!isDraft(value)) die(23, value); + return value[DRAFT_STATE].base_; + } + + + var ownKeys$a = typeof Reflect !== "undefined" && Reflect.ownKeys ? Reflect.ownKeys : typeof Object.getOwnPropertySymbols !== "undefined" ? function (obj) { + return Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj)); + } : + /* istanbul ignore next */ + Object.getOwnPropertyNames; + var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors(target) { + // Polyfill needed for Hermes and IE, see https://github.com/facebook/hermes/issues/274 + var res = {}; + ownKeys$a(target).forEach(function (key) { + res[key] = Object.getOwnPropertyDescriptor(target, key); + }); + return res; + }; + function each$1(obj, iter, enumerableOnly) { + if (enumerableOnly === void 0) { + enumerableOnly = false; + } + + if (getArchtype(obj) === 0 + /* Object */ + ) { + (enumerableOnly ? Object.keys : ownKeys$a)(obj).forEach(function (key) { + if (!enumerableOnly || typeof key !== "symbol") iter(key, obj[key], obj); + }); + } else { + obj.forEach(function (entry, index) { + return iter(index, entry, obj); + }); + } + } + + + function getArchtype(thing) { + /* istanbul ignore next */ + var state = thing[DRAFT_STATE]; + return state ? state.type_ > 3 ? state.type_ - 4 // cause Object and Array map back from 4 and 5 + : state.type_ // others are the same + : Array.isArray(thing) ? 1 + /* Array */ + : isMap(thing) ? 2 + /* Map */ + : isSet(thing) ? 3 + /* Set */ + : 0 + /* Object */ + ; + } + + + function has(thing, prop) { + return getArchtype(thing) === 2 + /* Map */ + ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop); + } + + + function get(thing, prop) { + // @ts-ignore + return getArchtype(thing) === 2 + /* Map */ + ? thing.get(prop) : thing[prop]; + } + + + function set(thing, propOrOldValue, value) { + var t = getArchtype(thing); + if (t === 2 + /* Map */ + ) thing.set(propOrOldValue, value);else if (t === 3 + /* Set */ + ) { + thing.delete(propOrOldValue); + thing.add(value); + } else thing[propOrOldValue] = value; + } + + + function is$1(x, y) { + // From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js + if (x === y) { + return x !== 0 || 1 / x === 1 / y; + } else { + return x !== x && y !== y; + } + } + + + function isMap(target) { + return hasMap && target instanceof Map; + } + + + function isSet(target) { + return hasSet && target instanceof Set; + } + + + function latest(state) { + return state.copy_ || state.base_; + } + + + function shallowCopy(base) { + if (Array.isArray(base)) return Array.prototype.slice.call(base); + var descriptors = getOwnPropertyDescriptors(base); + delete descriptors[DRAFT_STATE]; + var keys = ownKeys$a(descriptors); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var desc = descriptors[key]; + + if (desc.writable === false) { + desc.writable = true; + desc.configurable = true; + } // like object.assign, we will read any _own_, get/set accessors. This helps in dealing + // with libraries that trap values, like mobx or vue + // unlike object.assign, non-enumerables will be copied as well + + + if (desc.get || desc.set) descriptors[key] = { + configurable: true, + writable: true, + enumerable: desc.enumerable, + value: base[key] + }; + } + + return Object.create(Object.getPrototypeOf(base), descriptors); + } + function freeze(obj, deep) { + if (deep === void 0) { + deep = false; + } + + if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj; + + if (getArchtype(obj) > 1 + /* Map or Set */ + ) { + obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections; + } + + Object.freeze(obj); + if (deep) each$1(obj, function (key, value) { + return freeze(value, true); + }, true); + return obj; + } + + function dontMutateFrozenCollections() { + die(2); + } + + function isFrozen(obj) { + if (obj == null || typeof obj !== "object") return true; // See #600, IE dies on non-objects in Object.isFrozen + + return Object.isFrozen(obj); + } + + /** Plugin utilities */ + + var plugins = {}; + function getPlugin(pluginKey) { + var plugin = plugins[pluginKey]; + + if (!plugin) { + die(18, pluginKey); + } // @ts-ignore + + + return plugin; + } + function loadPlugin(pluginKey, implementation) { + if (!plugins[pluginKey]) plugins[pluginKey] = implementation; + } + + var currentScope; + function getCurrentScope() { + if ( !currentScope) die(0); + return currentScope; + } + + function createScope(parent_, immer_) { + return { + drafts_: [], + parent_: parent_, + immer_: immer_, + // Whenever the modified draft contains a draft from another scope, we + // need to prevent auto-freezing so the unowned draft can be finalized. + canAutoFreeze_: true, + unfinalizedDrafts_: 0 + }; + } + + function usePatchesInScope(scope, patchListener) { + if (patchListener) { + getPlugin("Patches"); // assert we have the plugin + + scope.patches_ = []; + scope.inversePatches_ = []; + scope.patchListener_ = patchListener; + } + } + function revokeScope(scope) { + leaveScope(scope); + scope.drafts_.forEach(revokeDraft); // @ts-ignore + + scope.drafts_ = null; + } + function leaveScope(scope) { + if (scope === currentScope) { + currentScope = scope.parent_; + } + } + function enterScope(immer) { + return currentScope = createScope(currentScope, immer); + } + + function revokeDraft(draft) { + var state = draft[DRAFT_STATE]; + if (state.type_ === 0 + /* ProxyObject */ + || state.type_ === 1 + /* ProxyArray */ + ) state.revoke_();else state.revoked_ = true; + } + + function processResult(result, scope) { + scope.unfinalizedDrafts_ = scope.drafts_.length; + var baseDraft = scope.drafts_[0]; + var isReplaced = result !== undefined && result !== baseDraft; + if (!scope.immer_.useProxies_) getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced); + + if (isReplaced) { + if (baseDraft[DRAFT_STATE].modified_) { + revokeScope(scope); + die(4); + } + + if (isDraftable(result)) { + // Finalize the result in case it contains (or is) a subset of the draft. + result = finalize(scope, result); + if (!scope.parent_) maybeFreeze(scope, result); + } + + if (scope.patches_) { + getPlugin("Patches").generateReplacementPatches_(baseDraft[DRAFT_STATE], result, scope.patches_, scope.inversePatches_); + } + } else { + // Finalize the base draft. + result = finalize(scope, baseDraft, []); + } + + revokeScope(scope); + + if (scope.patches_) { + scope.patchListener_(scope.patches_, scope.inversePatches_); + } + + return result !== NOTHING ? result : undefined; + } + + function finalize(rootScope, value, path) { + // Don't recurse in tho recursive data structures + if (isFrozen(value)) return value; + var state = value[DRAFT_STATE]; // A plain object, might need freezing, might contain drafts + + if (!state) { + each$1(value, function (key, childValue) { + return finalizeProperty(rootScope, state, value, key, childValue, path); + }, true // See #590, don't recurse into non-enumerable of non drafted objects + ); + return value; + } // Never finalize drafts owned by another scope. + + + if (state.scope_ !== rootScope) return value; // Unmodified draft, return the (frozen) original + + if (!state.modified_) { + maybeFreeze(rootScope, state.base_, true); + return state.base_; + } // Not finalized yet, let's do that now + + + if (!state.finalized_) { + state.finalized_ = true; + state.scope_.unfinalizedDrafts_--; + var result = // For ES5, create a good copy from the draft first, with added keys and without deleted keys. + state.type_ === 4 + /* ES5Object */ + || state.type_ === 5 + /* ES5Array */ + ? state.copy_ = shallowCopy(state.draft_) : state.copy_; // Finalize all children of the copy + // For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628 + // Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line + // back to each(result, ....) + + each$1(state.type_ === 3 + /* Set */ + ? new Set(result) : result, function (key, childValue) { + return finalizeProperty(rootScope, state, result, key, childValue, path); + }); // everything inside is frozen, we can freeze here + + maybeFreeze(rootScope, result, false); // first time finalizing, let's create those patches + + if (path && rootScope.patches_) { + getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_); + } + } + + return state.copy_; + } + + function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath) { + if ( childValue === targetObject) die(5); + + if (isDraft(childValue)) { + var path = rootPath && parentState && parentState.type_ !== 3 + /* Set */ + && // Set objects are atomic since they have no keys. + !has(parentState.assigned_, prop) // Skip deep patches for assigned keys. + ? rootPath.concat(prop) : undefined; // Drafts owned by `scope` are finalized here. + + var res = finalize(rootScope, childValue, path); + set(targetObject, prop, res); // Drafts from another scope must prevented to be frozen + // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze + + if (isDraft(res)) { + rootScope.canAutoFreeze_ = false; + } else return; + } // Search new objects for unfinalized drafts. Frozen objects should never contain drafts. + + + if (isDraftable(childValue) && !isFrozen(childValue)) { + if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) { + // optimization: if an object is not a draft, and we don't have to + // deepfreeze everything, and we are sure that no drafts are left in the remaining object + // cause we saw and finalized all drafts already; we can stop visiting the rest of the tree. + // This benefits especially adding large data tree's without further processing. + // See add-data.js perf test + return; + } + + finalize(rootScope, childValue); // immer deep freezes plain objects, so if there is no parent state, we freeze as well + + if (!parentState || !parentState.scope_.parent_) maybeFreeze(rootScope, childValue); + } + } + + function maybeFreeze(scope, value, deep) { + if (deep === void 0) { + deep = false; + } + + if (scope.immer_.autoFreeze_ && scope.canAutoFreeze_) { + freeze(value, deep); + } + } + + /** + * Returns a new draft of the `base` object. + * + * The second argument is the parent draft-state (used internally). + */ + + function createProxyProxy(base, parent) { + var isArray = Array.isArray(base); + var state = { + type_: isArray ? 1 + /* ProxyArray */ + : 0 + /* ProxyObject */ + , + // Track which produce call this is associated with. + scope_: parent ? parent.scope_ : getCurrentScope(), + // True for both shallow and deep changes. + modified_: false, + // Used during finalization. + finalized_: false, + // Track which properties have been assigned (true) or deleted (false). + assigned_: {}, + // The parent draft state. + parent_: parent, + // The base state. + base_: base, + // The base proxy. + draft_: null, + // The base copy with any updated values. + copy_: null, + // Called by the `produce` function. + revoke_: null, + isManual_: false + }; // the traps must target something, a bit like the 'real' base. + // but also, we need to be able to determine from the target what the relevant state is + // (to avoid creating traps per instance to capture the state in closure, + // and to avoid creating weird hidden properties as well) + // So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything) + // Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb + + var target = state; + var traps = objectTraps; + + if (isArray) { + target = [state]; + traps = arrayTraps; + } + + var _Proxy$revocable = Proxy.revocable(target, traps), + revoke = _Proxy$revocable.revoke, + proxy = _Proxy$revocable.proxy; + + state.draft_ = proxy; + state.revoke_ = revoke; + return proxy; + } + /** + * Object drafts + */ + + var objectTraps = { + get: function get(state, prop) { + if (prop === DRAFT_STATE) return state; + var source = latest(state); + + if (!has(source, prop)) { + // non-existing or non-own property... + return readPropFromProto(state, source, prop); + } + + var value = source[prop]; + + if (state.finalized_ || !isDraftable(value)) { + return value; + } // Check for existing draft in modified state. + // Assigned values are never drafted. This catches any drafts we created, too. + + + if (value === peek(state.base_, prop)) { + prepareCopy(state); + return state.copy_[prop] = createProxy(state.scope_.immer_, value, state); + } + + return value; + }, + has: function has(state, prop) { + return prop in latest(state); + }, + ownKeys: function ownKeys(state) { + return Reflect.ownKeys(latest(state)); + }, + set: function set(state, prop + /* strictly not, but helps TS */ + , value) { + var desc = getDescriptorFromProto(latest(state), prop); + + if (desc === null || desc === void 0 ? void 0 : desc.set) { + // special case: if this write is captured by a setter, we have + // to trigger it with the correct context + desc.set.call(state.draft_, value); + return true; + } + + if (!state.modified_) { + // the last check is because we need to be able to distinguish setting a non-existing to undefined (which is a change) + // from setting an existing property with value undefined to undefined (which is not a change) + var current = peek(latest(state), prop); // special case, if we assigning the original value to a draft, we can ignore the assignment + + var currentState = current === null || current === void 0 ? void 0 : current[DRAFT_STATE]; + + if (currentState && currentState.base_ === value) { + state.copy_[prop] = value; + state.assigned_[prop] = false; + return true; + } + + if (is$1(value, current) && (value !== undefined || has(state.base_, prop))) return true; + prepareCopy(state); + markChanged(state); + } + + if (state.copy_[prop] === value && // special case: NaN + typeof value !== "number" && ( // special case: handle new props with value 'undefined' + value !== undefined || prop in state.copy_)) return true; // @ts-ignore + + state.copy_[prop] = value; + state.assigned_[prop] = true; + return true; + }, + deleteProperty: function deleteProperty(state, prop) { + // The `undefined` check is a fast path for pre-existing keys. + if (peek(state.base_, prop) !== undefined || prop in state.base_) { + state.assigned_[prop] = false; + prepareCopy(state); + markChanged(state); + } else { + // if an originally not assigned property was deleted + delete state.assigned_[prop]; + } // @ts-ignore + + + if (state.copy_) delete state.copy_[prop]; + return true; + }, + // Note: We never coerce `desc.value` into an Immer draft, because we can't make + // the same guarantee in ES5 mode. + getOwnPropertyDescriptor: function getOwnPropertyDescriptor(state, prop) { + var owner = latest(state); + var desc = Reflect.getOwnPropertyDescriptor(owner, prop); + if (!desc) return desc; + return { + writable: true, + configurable: state.type_ !== 1 + /* ProxyArray */ + || prop !== "length", + enumerable: desc.enumerable, + value: owner[prop] + }; + }, + defineProperty: function defineProperty() { + die(11); + }, + getPrototypeOf: function getPrototypeOf(state) { + return Object.getPrototypeOf(state.base_); + }, + setPrototypeOf: function setPrototypeOf() { + die(12); + } + }; + /** + * Array drafts + */ + + var arrayTraps = {}; + each$1(objectTraps, function (key, fn) { + // @ts-ignore + arrayTraps[key] = function () { + arguments[0] = arguments[0][0]; + return fn.apply(this, arguments); + }; + }); + + arrayTraps.deleteProperty = function (state, prop) { + if ( isNaN(parseInt(prop))) die(13); + return objectTraps.deleteProperty.call(this, state[0], prop); + }; + + arrayTraps.set = function (state, prop, value) { + if ( prop !== "length" && isNaN(parseInt(prop))) die(14); + return objectTraps.set.call(this, state[0], prop, value, state[0]); + }; // Access a property without creating an Immer draft. + + + function peek(draft, prop) { + var state = draft[DRAFT_STATE]; + var source = state ? latest(state) : draft; + return source[prop]; + } + + function readPropFromProto(state, source, prop) { + var _desc$get; + + var desc = getDescriptorFromProto(source, prop); + return desc ? "value" in desc ? desc.value : // This is a very special case, if the prop is a getter defined by the + // prototype, we should invoke it with the draft as context! + (_desc$get = desc.get) === null || _desc$get === void 0 ? void 0 : _desc$get.call(state.draft_) : undefined; + } + + function getDescriptorFromProto(source, prop) { + // 'in' checks proto! + if (!(prop in source)) return undefined; + var proto = Object.getPrototypeOf(source); + + while (proto) { + var desc = Object.getOwnPropertyDescriptor(proto, prop); + if (desc) return desc; + proto = Object.getPrototypeOf(proto); + } + + return undefined; + } + + function markChanged(state) { + if (!state.modified_) { + state.modified_ = true; + + if (state.parent_) { + markChanged(state.parent_); + } + } + } + function prepareCopy(state) { + if (!state.copy_) { + state.copy_ = shallowCopy(state.base_); + } + } + + var Immer = + /*#__PURE__*/ + function () { + function Immer(config) { + var _this = this; + + this.useProxies_ = hasProxies; + this.autoFreeze_ = true; + /** + * The `produce` function takes a value and a "recipe function" (whose + * return value often depends on the base state). The recipe function is + * free to mutate its first argument however it wants. All mutations are + * only ever applied to a __copy__ of the base state. + * + * Pass only a function to create a "curried producer" which relieves you + * from passing the recipe function every time. + * + * Only plain objects and arrays are made mutable. All other objects are + * considered uncopyable. + * + * Note: This function is __bound__ to its `Immer` instance. + * + * @param {any} base - the initial state + * @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified + * @param {Function} patchListener - optional function that will be called with all the patches produced here + * @returns {any} a new state, or the initial state if nothing was modified + */ + + this.produce = function (base, recipe, patchListener) { + // curried invocation + if (typeof base === "function" && typeof recipe !== "function") { + var defaultBase = recipe; + recipe = base; + var self = _this; + return function curriedProduce(base) { + var _this2 = this; + + if (base === void 0) { + base = defaultBase; + } + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + return self.produce(base, function (draft) { + var _recipe; + + return (_recipe = recipe).call.apply(_recipe, [_this2, draft].concat(args)); + }); // prettier-ignore + }; + } + + if (typeof recipe !== "function") die(6); + if (patchListener !== undefined && typeof patchListener !== "function") die(7); + var result; // Only plain objects, arrays, and "immerable classes" are drafted. + + if (isDraftable(base)) { + var scope = enterScope(_this); + var proxy = createProxy(_this, base, undefined); + var hasError = true; + + try { + result = recipe(proxy); + hasError = false; + } finally { + // finally instead of catch + rethrow better preserves original stack + if (hasError) revokeScope(scope);else leaveScope(scope); + } + + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then(function (result) { + usePatchesInScope(scope, patchListener); + return processResult(result, scope); + }, function (error) { + revokeScope(scope); + throw error; + }); + } + + usePatchesInScope(scope, patchListener); + return processResult(result, scope); + } else if (!base || typeof base !== "object") { + result = recipe(base); + if (result === NOTHING) return undefined; + if (result === undefined) result = base; + if (_this.autoFreeze_) freeze(result, true); + return result; + } else die(21, base); + }; + + this.produceWithPatches = function (arg1, arg2, arg3) { + if (typeof arg1 === "function") { + return function (state) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + + return _this.produceWithPatches(state, function (draft) { + return arg1.apply(void 0, [draft].concat(args)); + }); + }; + } + + var patches, inversePatches; + + var nextState = _this.produce(arg1, arg2, function (p, ip) { + patches = p; + inversePatches = ip; + }); + + return [nextState, patches, inversePatches]; + }; + + if (typeof (config === null || config === void 0 ? void 0 : config.useProxies) === "boolean") this.setUseProxies(config.useProxies); + if (typeof (config === null || config === void 0 ? void 0 : config.autoFreeze) === "boolean") this.setAutoFreeze(config.autoFreeze); + } + + var _proto = Immer.prototype; + + _proto.createDraft = function createDraft(base) { + if (!isDraftable(base)) die(8); + if (isDraft(base)) base = current(base); + var scope = enterScope(this); + var proxy = createProxy(this, base, undefined); + proxy[DRAFT_STATE].isManual_ = true; + leaveScope(scope); + return proxy; + }; + + _proto.finishDraft = function finishDraft(draft, patchListener) { + var state = draft && draft[DRAFT_STATE]; + + { + if (!state || !state.isManual_) die(9); + if (state.finalized_) die(10); + } + + var scope = state.scope_; + usePatchesInScope(scope, patchListener); + return processResult(undefined, scope); + } + /** + * Pass true to automatically freeze all copies created by Immer. + * + * By default, auto-freezing is enabled. + */ + ; + + _proto.setAutoFreeze = function setAutoFreeze(value) { + this.autoFreeze_ = value; + } + /** + * Pass true to use the ES2015 `Proxy` class when creating drafts, which is + * always faster than using ES5 proxies. + * + * By default, feature detection is used, so calling this is rarely necessary. + */ + ; + + _proto.setUseProxies = function setUseProxies(value) { + if (value && !hasProxies) { + die(20); + } + + this.useProxies_ = value; + }; + + _proto.applyPatches = function applyPatches(base, patches) { + // If a patch replaces the entire state, take that replacement as base + // before applying patches + var i; + + for (i = patches.length - 1; i >= 0; i--) { + var patch = patches[i]; + + if (patch.path.length === 0 && patch.op === "replace") { + base = patch.value; + break; + } + } // If there was a patch that replaced the entire state, start from the + // patch after that. + + + if (i > -1) { + patches = patches.slice(i + 1); + } + + var applyPatchesImpl = getPlugin("Patches").applyPatches_; + + if (isDraft(base)) { + // N.B: never hits if some patch a replacement, patches are never drafts + return applyPatchesImpl(base, patches); + } // Otherwise, produce a copy of the base state. + + + return this.produce(base, function (draft) { + return applyPatchesImpl(draft, patches); + }); + }; + + return Immer; + }(); + function createProxy(immer, value, parent) { + // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft + var draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : immer.useProxies_ ? createProxyProxy(value, parent) : getPlugin("ES5").createES5Proxy_(value, parent); + var scope = parent ? parent.scope_ : getCurrentScope(); + scope.drafts_.push(draft); + return draft; + } + + function current(value) { + if (!isDraft(value)) die(22, value); + return currentImpl(value); + } + + function currentImpl(value) { + if (!isDraftable(value)) return value; + var state = value[DRAFT_STATE]; + var copy; + var archType = getArchtype(value); + + if (state) { + if (!state.modified_ && (state.type_ < 4 || !getPlugin("ES5").hasChanges_(state))) return state.base_; // Optimization: avoid generating new drafts during copying + + state.finalized_ = true; + copy = copyHelper(value, archType); + state.finalized_ = false; + } else { + copy = copyHelper(value, archType); + } + + each$1(copy, function (key, childValue) { + if (state && get(state.base_, key) === childValue) return; // no need to copy or search in something that didn't change + + set(copy, key, currentImpl(childValue)); + }); // In the future, we might consider freezing here, based on the current settings + + return archType === 3 + /* Set */ + ? new Set(copy) : copy; + } + + function copyHelper(value, archType) { + // creates a shallow copy, even if it is a map or set + switch (archType) { + case 2 + /* Map */ + : + return new Map(value); + + case 3 + /* Set */ + : + // Set will be cloned as array temporarily, so that we can replace individual items + return Array.from(value); + } + + return shallowCopy(value); + } + + function enableES5() { + function willFinalizeES5_(scope, result, isReplaced) { + if (!isReplaced) { + if (scope.patches_) { + markChangesRecursively(scope.drafts_[0]); + } // This is faster when we don't care about which attributes changed. + + + markChangesSweep(scope.drafts_); + } // When a child draft is returned, look for changes. + else if (isDraft(result) && result[DRAFT_STATE].scope_ === scope) { + markChangesSweep(scope.drafts_); + } + } + + function createES5Draft(isArray, base) { + if (isArray) { + var draft = new Array(base.length); + + for (var i = 0; i < base.length; i++) { + Object.defineProperty(draft, "" + i, proxyProperty(i, true)); + } + + return draft; + } else { + var _descriptors = getOwnPropertyDescriptors(base); + + delete _descriptors[DRAFT_STATE]; + var keys = ownKeys$a(_descriptors); + + for (var _i = 0; _i < keys.length; _i++) { + var key = keys[_i]; + _descriptors[key] = proxyProperty(key, isArray || !!_descriptors[key].enumerable); + } + + return Object.create(Object.getPrototypeOf(base), _descriptors); + } + } + + function createES5Proxy_(base, parent) { + var isArray = Array.isArray(base); + var draft = createES5Draft(isArray, base); + var state = { + type_: isArray ? 5 + /* ES5Array */ + : 4 + /* ES5Object */ + , + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + assigned_: {}, + parent_: parent, + // base is the object we are drafting + base_: base, + // draft is the draft object itself, that traps all reads and reads from either the base (if unmodified) or copy (if modified) + draft_: draft, + copy_: null, + revoked_: false, + isManual_: false + }; + Object.defineProperty(draft, DRAFT_STATE, { + value: state, + // enumerable: false <- the default + writable: true + }); + return draft; + } // property descriptors are recycled to make sure we don't create a get and set closure per property, + // but share them all instead + + + var descriptors = {}; + + function proxyProperty(prop, enumerable) { + var desc = descriptors[prop]; + + if (desc) { + desc.enumerable = enumerable; + } else { + descriptors[prop] = desc = { + configurable: true, + enumerable: enumerable, + get: function get() { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // @ts-ignore + + return objectTraps.get(state, prop); + }, + set: function set(value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // @ts-ignore + + objectTraps.set(state, prop, value); + } + }; + } + + return desc; + } // This looks expensive, but only proxies are visited, and only objects without known changes are scanned. + + + function markChangesSweep(drafts) { + // The natural order of drafts in the `scope` array is based on when they + // were accessed. By processing drafts in reverse natural order, we have a + // better chance of processing leaf nodes first. When a leaf node is known to + // have changed, we can avoid any traversal of its ancestor nodes. + for (var i = drafts.length - 1; i >= 0; i--) { + var state = drafts[i][DRAFT_STATE]; + + if (!state.modified_) { + switch (state.type_) { + case 5 + /* ES5Array */ + : + if (hasArrayChanges(state)) markChanged(state); + break; + + case 4 + /* ES5Object */ + : + if (hasObjectChanges(state)) markChanged(state); + break; + } + } + } + } + + function markChangesRecursively(object) { + if (!object || typeof object !== "object") return; + var state = object[DRAFT_STATE]; + if (!state) return; + var base_ = state.base_, + draft_ = state.draft_, + assigned_ = state.assigned_, + type_ = state.type_; + + if (type_ === 4 + /* ES5Object */ + ) { + // Look for added keys. + // probably there is a faster way to detect changes, as sweep + recurse seems to do some + // unnecessary work. + // also: probably we can store the information we detect here, to speed up tree finalization! + each$1(draft_, function (key) { + if (key === DRAFT_STATE) return; // The `undefined` check is a fast path for pre-existing keys. + + if (base_[key] === undefined && !has(base_, key)) { + assigned_[key] = true; + markChanged(state); + } else if (!assigned_[key]) { + // Only untouched properties trigger recursion. + markChangesRecursively(draft_[key]); + } + }); // Look for removed keys. + + each$1(base_, function (key) { + // The `undefined` check is a fast path for pre-existing keys. + if (draft_[key] === undefined && !has(draft_, key)) { + assigned_[key] = false; + markChanged(state); + } + }); + } else if (type_ === 5 + /* ES5Array */ + ) { + if (hasArrayChanges(state)) { + markChanged(state); + assigned_.length = true; + } + + if (draft_.length < base_.length) { + for (var i = draft_.length; i < base_.length; i++) { + assigned_[i] = false; + } + } else { + for (var _i2 = base_.length; _i2 < draft_.length; _i2++) { + assigned_[_i2] = true; + } + } // Minimum count is enough, the other parts has been processed. + + + var min = Math.min(draft_.length, base_.length); + + for (var _i3 = 0; _i3 < min; _i3++) { + // Only untouched indices trigger recursion. + if (assigned_[_i3] === undefined) markChangesRecursively(draft_[_i3]); + } + } + } + + function hasObjectChanges(state) { + var base_ = state.base_, + draft_ = state.draft_; // Search for added keys and changed keys. Start at the back, because + // non-numeric keys are ordered by time of definition on the object. + + var keys = ownKeys$a(draft_); + + for (var i = keys.length - 1; i >= 0; i--) { + var key = keys[i]; + if (key === DRAFT_STATE) continue; + var baseValue = base_[key]; // The `undefined` check is a fast path for pre-existing keys. + + if (baseValue === undefined && !has(base_, key)) { + return true; + } // Once a base key is deleted, future changes go undetected, because its + // descriptor is erased. This branch detects any missed changes. + else { + var value = draft_[key]; + + var _state = value && value[DRAFT_STATE]; + + if (_state ? _state.base_ !== baseValue : !is$1(value, baseValue)) { + return true; + } + } + } // At this point, no keys were added or changed. + // Compare key count to determine if keys were deleted. + + + var baseIsDraft = !!base_[DRAFT_STATE]; + return keys.length !== ownKeys$a(base_).length + (baseIsDraft ? 0 : 1); // + 1 to correct for DRAFT_STATE + } + + function hasArrayChanges(state) { + var draft_ = state.draft_; + if (draft_.length !== state.base_.length) return true; // See #116 + // If we first shorten the length, our array interceptors will be removed. + // If after that new items are added, result in the same original length, + // those last items will have no intercepting property. + // So if there is no own descriptor on the last position, we know that items were removed and added + // N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check + // the last one + + var descriptor = Object.getOwnPropertyDescriptor(draft_, draft_.length - 1); // descriptor can be null, but only for newly created sparse arrays, eg. new Array(10) + + if (descriptor && !descriptor.get) return true; // For all other cases, we don't have to compare, as they would have been picked up by the index setters + + return false; + } + + function hasChanges_(state) { + return state.type_ === 4 + /* ES5Object */ + ? hasObjectChanges(state) : hasArrayChanges(state); + } + + function assertUnrevoked(state + /*ES5State | MapState | SetState*/ + ) { + if (state.revoked_) die(3, JSON.stringify(latest(state))); + } + + loadPlugin("ES5", { + createES5Proxy_: createES5Proxy_, + willFinalizeES5_: willFinalizeES5_, + hasChanges_: hasChanges_ + }); + } + + function enablePatches() { + var REPLACE = "replace"; + var ADD = "add"; + var REMOVE = "remove"; + + function generatePatches_(state, basePath, patches, inversePatches) { + switch (state.type_) { + case 0 + /* ProxyObject */ + : + case 4 + /* ES5Object */ + : + case 2 + /* Map */ + : + return generatePatchesFromAssigned(state, basePath, patches, inversePatches); + + case 5 + /* ES5Array */ + : + case 1 + /* ProxyArray */ + : + return generateArrayPatches(state, basePath, patches, inversePatches); + + case 3 + /* Set */ + : + return generateSetPatches(state, basePath, patches, inversePatches); + } + } + + function generateArrayPatches(state, basePath, patches, inversePatches) { + var base_ = state.base_, + assigned_ = state.assigned_; + var copy_ = state.copy_; // Reduce complexity by ensuring `base` is never longer. + + if (copy_.length < base_.length) { + var _ref = [copy_, base_]; + base_ = _ref[0]; + copy_ = _ref[1]; + var _ref2 = [inversePatches, patches]; + patches = _ref2[0]; + inversePatches = _ref2[1]; + } // Process replaced indices. + + + for (var i = 0; i < base_.length; i++) { + if (assigned_[i] && copy_[i] !== base_[i]) { + var path = basePath.concat([i]); + patches.push({ + op: REPLACE, + path: path, + // Need to maybe clone it, as it can in fact be the original value + // due to the base/copy inversion at the start of this function + value: clonePatchValueIfNeeded(copy_[i]) + }); + inversePatches.push({ + op: REPLACE, + path: path, + value: clonePatchValueIfNeeded(base_[i]) + }); + } + } // Process added indices. + + + for (var _i = base_.length; _i < copy_.length; _i++) { + var _path = basePath.concat([_i]); + + patches.push({ + op: ADD, + path: _path, + // Need to maybe clone it, as it can in fact be the original value + // due to the base/copy inversion at the start of this function + value: clonePatchValueIfNeeded(copy_[_i]) + }); + } + + if (base_.length < copy_.length) { + inversePatches.push({ + op: REPLACE, + path: basePath.concat(["length"]), + value: base_.length + }); + } + } // This is used for both Map objects and normal objects. + + + function generatePatchesFromAssigned(state, basePath, patches, inversePatches) { + var base_ = state.base_, + copy_ = state.copy_; + each$1(state.assigned_, function (key, assignedValue) { + var origValue = get(base_, key); + var value = get(copy_, key); + var op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD; + if (origValue === value && op === REPLACE) return; + var path = basePath.concat(key); + patches.push(op === REMOVE ? { + op: op, + path: path + } : { + op: op, + path: path, + value: value + }); + inversePatches.push(op === ADD ? { + op: REMOVE, + path: path + } : op === REMOVE ? { + op: ADD, + path: path, + value: clonePatchValueIfNeeded(origValue) + } : { + op: REPLACE, + path: path, + value: clonePatchValueIfNeeded(origValue) + }); + }); + } + + function generateSetPatches(state, basePath, patches, inversePatches) { + var base_ = state.base_, + copy_ = state.copy_; + var i = 0; + base_.forEach(function (value) { + if (!copy_.has(value)) { + var path = basePath.concat([i]); + patches.push({ + op: REMOVE, + path: path, + value: value + }); + inversePatches.unshift({ + op: ADD, + path: path, + value: value + }); + } + + i++; + }); + i = 0; + copy_.forEach(function (value) { + if (!base_.has(value)) { + var path = basePath.concat([i]); + patches.push({ + op: ADD, + path: path, + value: value + }); + inversePatches.unshift({ + op: REMOVE, + path: path, + value: value + }); + } + + i++; + }); + } + + function generateReplacementPatches_(rootState, replacement, patches, inversePatches) { + patches.push({ + op: REPLACE, + path: [], + value: replacement === NOTHING ? undefined : replacement + }); + inversePatches.push({ + op: REPLACE, + path: [], + value: rootState.base_ + }); + } + + function applyPatches_(draft, patches) { + patches.forEach(function (patch) { + var path = patch.path, + op = patch.op; + var base = draft; + + for (var i = 0; i < path.length - 1; i++) { + var parentType = getArchtype(base); + var p = "" + path[i]; // See #738, avoid prototype pollution + + if ((parentType === 0 + /* Object */ + || parentType === 1 + /* Array */ + ) && (p === "__proto__" || p === "constructor")) die(24); + if (typeof base === "function" && p === "prototype") die(24); + base = get(base, p); + if (typeof base !== "object") die(15, path.join("/")); + } + + var type = getArchtype(base); + var value = deepClonePatchValue(patch.value); // used to clone patch to ensure original patch is not modified, see #411 + + var key = path[path.length - 1]; + + switch (op) { + case REPLACE: + switch (type) { + case 2 + /* Map */ + : + return base.set(key, value); + + /* istanbul ignore next */ + + case 3 + /* Set */ + : + die(16); + + default: + // if value is an object, then it's assigned by reference + // in the following add or remove ops, the value field inside the patch will also be modifyed + // so we use value from the cloned patch + // @ts-ignore + return base[key] = value; + } + + case ADD: + switch (type) { + case 1 + /* Array */ + : + return key === "-" ? base.push(value) : base.splice(key, 0, value); + + case 2 + /* Map */ + : + return base.set(key, value); + + case 3 + /* Set */ + : + return base.add(value); + + default: + return base[key] = value; + } + + case REMOVE: + switch (type) { + case 1 + /* Array */ + : + return base.splice(key, 1); + + case 2 + /* Map */ + : + return base.delete(key); + + case 3 + /* Set */ + : + return base.delete(patch.value); + + default: + return delete base[key]; + } + + default: + die(17, op); + } + }); + return draft; + } + + function deepClonePatchValue(obj) { + if (!isDraftable(obj)) return obj; + if (Array.isArray(obj)) return obj.map(deepClonePatchValue); + if (isMap(obj)) return new Map(Array.from(obj.entries()).map(function (_ref3) { + var k = _ref3[0], + v = _ref3[1]; + return [k, deepClonePatchValue(v)]; + })); + if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue)); + var cloned = Object.create(Object.getPrototypeOf(obj)); + + for (var key in obj) { + cloned[key] = deepClonePatchValue(obj[key]); + } + + if (has(obj, DRAFTABLE)) cloned[DRAFTABLE] = obj[DRAFTABLE]; + return cloned; + } + + function clonePatchValueIfNeeded(obj) { + if (isDraft(obj)) { + return deepClonePatchValue(obj); + } else return obj; + } + + loadPlugin("Patches", { + applyPatches_: applyPatches_, + generatePatches_: generatePatches_, + generateReplacementPatches_: generateReplacementPatches_ + }); + } + + // types only! + function enableMapSet() { + /* istanbul ignore next */ + var _extendStatics = function extendStatics(d, b) { + _extendStatics = Object.setPrototypeOf || { + __proto__: [] + } instanceof Array && function (d, b) { + d.__proto__ = b; + } || function (d, b) { + for (var p in b) { + if (b.hasOwnProperty(p)) d[p] = b[p]; + } + }; + + return _extendStatics(d, b); + }; // Ugly hack to resolve #502 and inherit built in Map / Set + + + function __extends(d, b) { + _extendStatics(d, b); + + function __() { + this.constructor = d; + } + + d.prototype = ( // @ts-ignore + __.prototype = b.prototype, new __()); + } + + var DraftMap = function (_super) { + __extends(DraftMap, _super); // Create class manually, cause #502 + + + function DraftMap(target, parent) { + this[DRAFT_STATE] = { + type_: 2 + /* Map */ + , + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + copy_: undefined, + assigned_: undefined, + base_: target, + draft_: this, + isManual_: false, + revoked_: false + }; + return this; + } + + var p = DraftMap.prototype; + Object.defineProperty(p, "size", { + get: function get() { + return latest(this[DRAFT_STATE]).size; + } // enumerable: false, + // configurable: true + + }); + + p.has = function (key) { + return latest(this[DRAFT_STATE]).has(key); + }; + + p.set = function (key, value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (!latest(state).has(key) || latest(state).get(key) !== value) { + prepareMapCopy(state); + markChanged(state); + state.assigned_.set(key, true); + state.copy_.set(key, value); + state.assigned_.set(key, true); + } + + return this; + }; + + p.delete = function (key) { + if (!this.has(key)) { + return false; + } + + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareMapCopy(state); + markChanged(state); + state.assigned_.set(key, false); + state.copy_.delete(key); + return true; + }; + + p.clear = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (latest(state).size) { + prepareMapCopy(state); + markChanged(state); + state.assigned_ = new Map(); + each$1(state.base_, function (key) { + state.assigned_.set(key, false); + }); + state.copy_.clear(); + } + }; + + p.forEach = function (cb, thisArg) { + var _this = this; + + var state = this[DRAFT_STATE]; + latest(state).forEach(function (_value, key, _map) { + cb.call(thisArg, _this.get(key), key, _this); + }); + }; + + p.get = function (key) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + var value = latest(state).get(key); + + if (state.finalized_ || !isDraftable(value)) { + return value; + } + + if (value !== state.base_.get(key)) { + return value; // either already drafted or reassigned + } // despite what it looks, this creates a draft only once, see above condition + + + var draft = createProxy(state.scope_.immer_, value, state); + prepareMapCopy(state); + state.copy_.set(key, draft); + return draft; + }; + + p.keys = function () { + return latest(this[DRAFT_STATE]).keys(); + }; + + p.values = function () { + var _this2 = this, + _ref; + + var iterator = this.keys(); + return _ref = {}, _ref[iteratorSymbol$1] = function () { + return _this2.values(); + }, _ref.next = function next() { + var r = iterator.next(); + /* istanbul ignore next */ + + if (r.done) return r; + + var value = _this2.get(r.value); + + return { + done: false, + value: value + }; + }, _ref; + }; + + p.entries = function () { + var _this3 = this, + _ref2; + + var iterator = this.keys(); + return _ref2 = {}, _ref2[iteratorSymbol$1] = function () { + return _this3.entries(); + }, _ref2.next = function next() { + var r = iterator.next(); + /* istanbul ignore next */ + + if (r.done) return r; + + var value = _this3.get(r.value); + + return { + done: false, + value: [r.value, value] + }; + }, _ref2; + }; + + p[iteratorSymbol$1] = function () { + return this.entries(); + }; + + return DraftMap; + }(Map); + + function proxyMap_(target, parent) { + // @ts-ignore + return new DraftMap(target, parent); + } + + function prepareMapCopy(state) { + if (!state.copy_) { + state.assigned_ = new Map(); + state.copy_ = new Map(state.base_); + } + } + + var DraftSet = function (_super) { + __extends(DraftSet, _super); // Create class manually, cause #502 + + + function DraftSet(target, parent) { + this[DRAFT_STATE] = { + type_: 3 + /* Set */ + , + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + copy_: undefined, + base_: target, + draft_: this, + drafts_: new Map(), + revoked_: false, + isManual_: false + }; + return this; + } + + var p = DraftSet.prototype; + Object.defineProperty(p, "size", { + get: function get() { + return latest(this[DRAFT_STATE]).size; + } // enumerable: true, + + }); + + p.has = function (value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // bit of trickery here, to be able to recognize both the value, and the draft of its value + + if (!state.copy_) { + return state.base_.has(value); + } + + if (state.copy_.has(value)) return true; + if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value))) return true; + return false; + }; + + p.add = function (value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (!this.has(value)) { + prepareSetCopy(state); + markChanged(state); + state.copy_.add(value); + } + + return this; + }; + + p.delete = function (value) { + if (!this.has(value)) { + return false; + } + + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + markChanged(state); + return state.copy_.delete(value) || (state.drafts_.has(value) ? state.copy_.delete(state.drafts_.get(value)) : + /* istanbul ignore next */ + false); + }; + + p.clear = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (latest(state).size) { + prepareSetCopy(state); + markChanged(state); + state.copy_.clear(); + } + }; + + p.values = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + return state.copy_.values(); + }; + + p.entries = function entries() { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + return state.copy_.entries(); + }; + + p.keys = function () { + return this.values(); + }; + + p[iteratorSymbol$1] = function () { + return this.values(); + }; + + p.forEach = function forEach(cb, thisArg) { + var iterator = this.values(); + var result = iterator.next(); + + while (!result.done) { + cb.call(thisArg, result.value, result.value, this); + result = iterator.next(); + } + }; + + return DraftSet; + }(Set); + + function proxySet_(target, parent) { + // @ts-ignore + return new DraftSet(target, parent); + } + + function prepareSetCopy(state) { + if (!state.copy_) { + // create drafts for all entries to preserve insertion order + state.copy_ = new Set(); + state.base_.forEach(function (value) { + if (isDraftable(value)) { + var draft = createProxy(state.scope_.immer_, value, state); + state.drafts_.set(value, draft); + state.copy_.add(draft); + } else { + state.copy_.add(value); + } + }); + } + } + + function assertUnrevoked(state + /*ES5State | MapState | SetState*/ + ) { + if (state.revoked_) die(3, JSON.stringify(latest(state))); + } + + loadPlugin("MapSet", { + proxyMap_: proxyMap_, + proxySet_: proxySet_ + }); + } + + function enableAllPlugins() { + enableES5(); + enableMapSet(); + enablePatches(); + } + + var immer$1 = + /*#__PURE__*/ + new Immer(); + /** + * The `produce` function takes a value and a "recipe function" (whose + * return value often depends on the base state). The recipe function is + * free to mutate its first argument however it wants. All mutations are + * only ever applied to a __copy__ of the base state. + * + * Pass only a function to create a "curried producer" which relieves you + * from passing the recipe function every time. + * + * Only plain objects and arrays are made mutable. All other objects are + * considered uncopyable. + * + * Note: This function is __bound__ to its `Immer` instance. + * + * @param {any} base - the initial state + * @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified + * @param {Function} patchListener - optional function that will be called with all the patches produced here + * @returns {any} a new state, or the initial state if nothing was modified + */ + + var produce = immer$1.produce; + /** + * Like `produce`, but `produceWithPatches` always returns a tuple + * [nextState, patches, inversePatches] (instead of just the next state) + */ + + var produceWithPatches = + /*#__PURE__*/ + immer$1.produceWithPatches.bind(immer$1); + /** + * Pass true to automatically freeze all copies created by Immer. + * + * Always freeze by default, even in production mode + */ + + var setAutoFreeze = + /*#__PURE__*/ + immer$1.setAutoFreeze.bind(immer$1); + /** + * Pass true to use the ES2015 `Proxy` class when creating drafts, which is + * always faster than using ES5 proxies. + * + * By default, feature detection is used, so calling this is rarely necessary. + */ + + var setUseProxies = + /*#__PURE__*/ + immer$1.setUseProxies.bind(immer$1); + /** + * Apply an array of Immer patches to the first argument. + * + * This function is a producer, which means copy-on-write is in effect. + */ + + var applyPatches = + /*#__PURE__*/ + immer$1.applyPatches.bind(immer$1); + /** + * Create an Immer draft from the given base state, which may be a draft itself. + * The draft can be modified until you finalize it with the `finishDraft` function. + */ + + var createDraft = + /*#__PURE__*/ + immer$1.createDraft.bind(immer$1); + /** + * Finalize an Immer draft from a `createDraft` call, returning the base state + * (if no changes were made) or a modified copy. The draft must *not* be + * mutated afterwards. + * + * Pass a function as the 2nd argument to generate Immer patches based on the + * changes that were made. + */ + + var finishDraft = + /*#__PURE__*/ + immer$1.finishDraft.bind(immer$1); + /** + * This function is actually a no-op, but can be used to cast an immutable type + * to an draft type and make TypeScript happy + * + * @param value + */ + + function castDraft(value) { + return value; + } + /** + * This function is actually a no-op, but can be used to cast a mutable type + * to an immutable type and make TypeScript happy + * @param value + */ + + function castImmutable(value) { + return value; + } + + var Immer_1 = Immer; + var applyPatches_1 = applyPatches; + var castDraft_1 = castDraft; + var castImmutable_1 = castImmutable; + var createDraft_1 = createDraft; + var current_1 = current; + var _default$2 = produce; + var enableAllPlugins_1 = enableAllPlugins; + var enableES5_1 = enableES5; + var enableMapSet_1 = enableMapSet; + var enablePatches_1 = enablePatches; + var finishDraft_1 = finishDraft; + var freeze_1 = freeze; + var immerable = DRAFTABLE; + var isDraft_1 = isDraft; + var isDraftable_1 = isDraftable; + var nothing = NOTHING; + var original_1 = original; + var produce_1 = produce; + var produceWithPatches_1 = produceWithPatches; + var setAutoFreeze_1 = setAutoFreeze; + var setUseProxies_1 = setUseProxies; + + + var immer_cjs_development = /*#__PURE__*/Object.defineProperty({ + Immer: Immer_1, + applyPatches: applyPatches_1, + castDraft: castDraft_1, + castImmutable: castImmutable_1, + createDraft: createDraft_1, + current: current_1, + default: _default$2, + enableAllPlugins: enableAllPlugins_1, + enableES5: enableES5_1, + enableMapSet: enableMapSet_1, + enablePatches: enablePatches_1, + finishDraft: finishDraft_1, + freeze: freeze_1, + immerable: immerable, + isDraft: isDraft_1, + isDraftable: isDraftable_1, + nothing: nothing, + original: original_1, + produce: produce_1, + produceWithPatches: produceWithPatches_1, + setAutoFreeze: setAutoFreeze_1, + setUseProxies: setUseProxies_1 + }, '__esModule', {value: true}); + + var require$$1$1 = immer_cjs_development; + + var dist$8 = createCommonjsModule$1(function (module) { + + { + module.exports = require$$1$1; + } + }); + + var isPlainObject = isPlainObject_1; + + var immer = dist$8; + + function unwrapExports (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var arrayLikeToArray = createCommonjsModule(function (module) { + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) { + arr2[i] = arr[i]; + } + + return arr2; + } + + module.exports = _arrayLikeToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayLikeToArray); + + var arrayWithoutHoles = createCommonjsModule(function (module) { + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return arrayLikeToArray(arr); + } + + module.exports = _arrayWithoutHoles; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayWithoutHoles); + + var iterableToArray = createCommonjsModule(function (module) { + function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + } + + module.exports = _iterableToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(iterableToArray); + + var unsupportedIterableToArray = createCommonjsModule(function (module) { + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen); + } + + module.exports = _unsupportedIterableToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(unsupportedIterableToArray); + + var nonIterableSpread = createCommonjsModule(function (module) { + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + module.exports = _nonIterableSpread; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(nonIterableSpread); + + var toConsumableArray = createCommonjsModule(function (module) { + function _toConsumableArray(arr) { + return arrayWithoutHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableSpread(); + } + + module.exports = _toConsumableArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _toConsumableArray = unwrapExports(toConsumableArray); + + var arrayWithHoles = createCommonjsModule(function (module) { + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + module.exports = _arrayWithHoles; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayWithHoles); + + var iterableToArrayLimit = createCommonjsModule(function (module) { + function _iterableToArrayLimit(arr, i) { + var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; + + if (_i == null) return; + var _arr = []; + var _n = true; + var _d = false; + + var _s, _e; + + try { + for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + module.exports = _iterableToArrayLimit; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(iterableToArrayLimit); + + var nonIterableRest = createCommonjsModule(function (module) { + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + module.exports = _nonIterableRest; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(nonIterableRest); + + var slicedToArray = createCommonjsModule(function (module) { + function _slicedToArray(arr, i) { + return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest(); + } + + module.exports = _slicedToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _slicedToArray = unwrapExports(slicedToArray); + + var defineProperty = createCommonjsModule(function (module) { + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + module.exports = _defineProperty; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _defineProperty = unwrapExports(defineProperty); + + var DIRTY_PATHS = new WeakMap(); + var FLUSHING = new WeakMap(); + var NORMALIZING = new WeakMap(); + var PATH_REFS = new WeakMap(); + var POINT_REFS = new WeakMap(); + var RANGE_REFS = new WeakMap(); + + function ownKeys$9(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$9(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$9(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$9(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$7(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$7(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$7(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$7(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$7(o, minLen); } + + function _arrayLikeToArray$7(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + /** + * Create a new Slate `Editor` object. + */ + + var createEditor$1 = function createEditor() { + var editor = { + children: [], + operations: [], + selection: null, + marks: null, + isInline: function isInline() { + return false; + }, + isVoid: function isVoid() { + return false; + }, + onChange: function onChange() {}, + apply: function apply(op) { + var _iterator = _createForOfIteratorHelper$7(Editor.pathRefs(editor)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var ref = _step.value; + PathRef.transform(ref, op); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + var _iterator2 = _createForOfIteratorHelper$7(Editor.pointRefs(editor)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _ref = _step2.value; + PointRef.transform(_ref, op); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + var _iterator3 = _createForOfIteratorHelper$7(Editor.rangeRefs(editor)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _ref2 = _step3.value; + RangeRef.transform(_ref2, op); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + var set = new Set(); + var dirtyPaths = []; + + var add = function add(path) { + if (path) { + var key = path.join(','); + + if (!set.has(key)) { + set.add(key); + dirtyPaths.push(path); + } + } + }; + + var oldDirtyPaths = DIRTY_PATHS.get(editor) || []; + var newDirtyPaths = getDirtyPaths(op); + + var _iterator4 = _createForOfIteratorHelper$7(oldDirtyPaths), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var path = _step4.value; + var newPath = Path.transform(path, op); + add(newPath); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + var _iterator5 = _createForOfIteratorHelper$7(newDirtyPaths), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _path = _step5.value; + add(_path); + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + DIRTY_PATHS.set(editor, dirtyPaths); + Transforms.transform(editor, op); + editor.operations.push(op); + Editor.normalize(editor); // Clear any formats applied to the cursor if the selection changes. + + if (op.type === 'set_selection') { + editor.marks = null; + } + + if (!FLUSHING.get(editor)) { + FLUSHING.set(editor, true); + Promise.resolve().then(function () { + FLUSHING.set(editor, false); + editor.onChange(); + editor.operations = []; + }); + } + }, + addMark: function addMark(key, value) { + var selection = editor.selection; + + if (selection) { + if (Range.isExpanded(selection)) { + Transforms.setNodes(editor, _defineProperty({}, key, value), { + match: Text.isText, + split: true + }); + } else { + var marks = _objectSpread$9(_objectSpread$9({}, Editor.marks(editor) || {}), {}, _defineProperty({}, key, value)); + + editor.marks = marks; + + if (!FLUSHING.get(editor)) { + editor.onChange(); + } + } + } + }, + deleteBackward: function deleteBackward(unit) { + var selection = editor.selection; + + if (selection && Range.isCollapsed(selection)) { + Transforms["delete"](editor, { + unit: unit, + reverse: true + }); + } + }, + deleteForward: function deleteForward(unit) { + var selection = editor.selection; + + if (selection && Range.isCollapsed(selection)) { + Transforms["delete"](editor, { + unit: unit + }); + } + }, + deleteFragment: function deleteFragment(direction) { + var selection = editor.selection; + + if (selection && Range.isExpanded(selection)) { + Transforms["delete"](editor, { + reverse: direction === 'backward' + }); + } + }, + getFragment: function getFragment() { + var selection = editor.selection; + + if (selection) { + return Node$1.fragment(editor, selection); + } + + return []; + }, + insertBreak: function insertBreak() { + Transforms.splitNodes(editor, { + always: true + }); + }, + insertFragment: function insertFragment(fragment) { + Transforms.insertFragment(editor, fragment); + }, + insertNode: function insertNode(node) { + Transforms.insertNodes(editor, node); + }, + insertText: function insertText(text) { + var selection = editor.selection, + marks = editor.marks; + + if (selection) { + if (marks) { + var node = _objectSpread$9({ + text: text + }, marks); + + Transforms.insertNodes(editor, node); + } else { + Transforms.insertText(editor, text); + } + + editor.marks = null; + } + }, + normalizeNode: function normalizeNode(entry) { + var _entry = _slicedToArray(entry, 2), + node = _entry[0], + path = _entry[1]; // There are no core normalizations for text nodes. + + + if (Text.isText(node)) { + return; + } // Ensure that block and inline nodes have at least one text child. + + + if (Element$1.isElement(node) && node.children.length === 0) { + var child = { + text: '' + }; + Transforms.insertNodes(editor, child, { + at: path.concat(0), + voids: true + }); + return; + } // Determine whether the node should have block or inline children. + + + var shouldHaveInlines = Editor.isEditor(node) ? false : Element$1.isElement(node) && (editor.isInline(node) || node.children.length === 0 || Text.isText(node.children[0]) || editor.isInline(node.children[0])); // Since we'll be applying operations while iterating, keep track of an + // index that accounts for any added/removed nodes. + + var n = 0; + + for (var i = 0; i < node.children.length; i++, n++) { + var currentNode = Node$1.get(editor, path); + if (Text.isText(currentNode)) continue; + var _child = node.children[i]; + var prev = currentNode.children[n - 1]; + var isLast = i === node.children.length - 1; + var isInlineOrText = Text.isText(_child) || Element$1.isElement(_child) && editor.isInline(_child); // Only allow block nodes in the top-level children and parent blocks + // that only contain block nodes. Similarly, only allow inline nodes in + // other inline nodes, or parent blocks that only contain inlines and + // text. + + if (isInlineOrText !== shouldHaveInlines) { + Transforms.removeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } else if (Element$1.isElement(_child)) { + // Ensure that inline nodes are surrounded by text nodes. + if (editor.isInline(_child)) { + if (prev == null || !Text.isText(prev)) { + var newChild = { + text: '' + }; + Transforms.insertNodes(editor, newChild, { + at: path.concat(n), + voids: true + }); + n++; + } else if (isLast) { + var _newChild = { + text: '' + }; + Transforms.insertNodes(editor, _newChild, { + at: path.concat(n + 1), + voids: true + }); + n++; + } + } + } else { + // Merge adjacent text nodes that are empty or match. + if (prev != null && Text.isText(prev)) { + if (Text.equals(_child, prev, { + loose: true + })) { + Transforms.mergeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } else if (prev.text === '') { + Transforms.removeNodes(editor, { + at: path.concat(n - 1), + voids: true + }); + n--; + } else if (_child.text === '') { + Transforms.removeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } + } + } + } + }, + removeMark: function removeMark(key) { + var selection = editor.selection; + + if (selection) { + if (Range.isExpanded(selection)) { + Transforms.unsetNodes(editor, key, { + match: Text.isText, + split: true + }); + } else { + var marks = _objectSpread$9({}, Editor.marks(editor) || {}); + + delete marks[key]; + editor.marks = marks; + + if (!FLUSHING.get(editor)) { + editor.onChange(); + } + } + } + } + }; + return editor; + }; + /** + * Get the "dirty" paths generated from an operation. + */ + + var getDirtyPaths = function getDirtyPaths(op) { + switch (op.type) { + case 'insert_text': + case 'remove_text': + case 'set_node': + { + var path = op.path; + return Path.levels(path); + } + + case 'insert_node': + { + var node = op.node, + _path2 = op.path; + var levels = Path.levels(_path2); + var descendants = Text.isText(node) ? [] : Array.from(Node$1.nodes(node), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + p = _ref4[1]; + + return _path2.concat(p); + }); + return [].concat(_toConsumableArray(levels), _toConsumableArray(descendants)); + } + + case 'merge_node': + { + var _path3 = op.path; + var ancestors = Path.ancestors(_path3); + var previousPath = Path.previous(_path3); + return [].concat(_toConsumableArray(ancestors), [previousPath]); + } + + case 'move_node': + { + var _path4 = op.path, + newPath = op.newPath; + + if (Path.equals(_path4, newPath)) { + return []; + } + + var oldAncestors = []; + var newAncestors = []; + + var _iterator6 = _createForOfIteratorHelper$7(Path.ancestors(_path4)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var ancestor = _step6.value; + var p = Path.transform(ancestor, op); + oldAncestors.push(p); + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var _iterator7 = _createForOfIteratorHelper$7(Path.ancestors(newPath)), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _ancestor = _step7.value; + + var _p = Path.transform(_ancestor, op); + + newAncestors.push(_p); + } + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + + var newParent = newAncestors[newAncestors.length - 1]; + var newIndex = newPath[newPath.length - 1]; + var resultPath = newParent.concat(newIndex); + return [].concat(oldAncestors, newAncestors, [resultPath]); + } + + case 'remove_node': + { + var _path5 = op.path; + + var _ancestors = Path.ancestors(_path5); + + return _toConsumableArray(_ancestors); + } + + case 'split_node': + { + var _path6 = op.path; + + var _levels = Path.levels(_path6); + + var nextPath = Path.next(_path6); + return [].concat(_toConsumableArray(_levels), [nextPath]); + } + + default: + { + return []; + } + } + }; + + var objectWithoutPropertiesLoose = createCommonjsModule(function (module) { + function _objectWithoutPropertiesLoose(source, excluded) { + if (source == null) return {}; + var target = {}; + var sourceKeys = Object.keys(source); + var key, i; + + for (i = 0; i < sourceKeys.length; i++) { + key = sourceKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + target[key] = source[key]; + } + + return target; + } + + module.exports = _objectWithoutPropertiesLoose; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(objectWithoutPropertiesLoose); + + var objectWithoutProperties = createCommonjsModule(function (module) { + function _objectWithoutProperties(source, excluded) { + if (source == null) return {}; + var target = objectWithoutPropertiesLoose(source, excluded); + var key, i; + + if (Object.getOwnPropertySymbols) { + var sourceSymbolKeys = Object.getOwnPropertySymbols(source); + + for (i = 0; i < sourceSymbolKeys.length; i++) { + key = sourceSymbolKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; + target[key] = source[key]; + } + } + + return target; + } + + module.exports = _objectWithoutProperties; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _objectWithoutProperties = unwrapExports(objectWithoutProperties); + + function _createForOfIteratorHelper$6(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$6(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$6(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$6(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$6(o, minLen); } + + function _arrayLikeToArray$6(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + // Character (grapheme cluster) boundaries are determined according to + // the default grapheme cluster boundary specification, extended grapheme clusters variant[1]. + // + // References: + // + // [1] https://www.unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table + // [2] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakProperty.txt + // [3] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.html + // [4] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.txt + + /** + * Get the distance to the end of the first character in a string of text. + */ + var getCharacterDistance = function getCharacterDistance(str) { + var isRTL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var isLTR = !isRTL; + var codepoints = isRTL ? codepointsIteratorRTL(str) : str; + var left = CodepointType.None; + var right = CodepointType.None; + var distance = 0; // Evaluation of these conditions are deferred. + + var gb11 = null; // Is GB11 applicable? + + var gb12Or13 = null; // Is GB12 or GB13 applicable? + + var _iterator = _createForOfIteratorHelper$6(codepoints), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _char = _step.value; + + var code = _char.codePointAt(0); + + if (!code) break; + var type = getCodepointType(_char, code); + + var _ref = isLTR ? [right, type] : [type, left]; + + var _ref2 = _slicedToArray(_ref, 2); + + left = _ref2[0]; + right = _ref2[1]; + + if (intersects(left, CodepointType.ZWJ) && intersects(right, CodepointType.ExtPict)) { + if (isLTR) { + gb11 = endsWithEmojiZWJ(str.substring(0, distance)); + } else { + gb11 = endsWithEmojiZWJ(str.substring(0, str.length - distance)); + } + + if (!gb11) break; + } + + if (intersects(left, CodepointType.RI) && intersects(right, CodepointType.RI)) { + if (gb12Or13 !== null) { + gb12Or13 = !gb12Or13; + } else { + if (isLTR) { + gb12Or13 = true; + } else { + gb12Or13 = endsWithOddNumberOfRIs(str.substring(0, str.length - distance)); + } + } + + if (!gb12Or13) break; + } + + if (left !== CodepointType.None && right !== CodepointType.None && isBoundaryPair(left, right)) { + break; + } + + distance += _char.length; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return distance || 1; + }; + var SPACE = /\s/; + var PUNCTUATION = /[\u0021-\u0023\u0025-\u002A\u002C-\u002F\u003A\u003B\u003F\u0040\u005B-\u005D\u005F\u007B\u007D\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/; + var CHAMELEON = /['\u2018\u2019]/; + /** + * Get the distance to the end of the first word in a string of text. + */ + + var getWordDistance = function getWordDistance(text) { + var isRTL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var dist = 0; + var started = false; + + while (text.length > 0) { + var charDist = getCharacterDistance(text, isRTL); + + var _splitByCharacterDist = splitByCharacterDistance(text, charDist, isRTL), + _splitByCharacterDist2 = _slicedToArray(_splitByCharacterDist, 2), + _char2 = _splitByCharacterDist2[0], + remaining = _splitByCharacterDist2[1]; + + if (isWordCharacter(_char2, remaining, isRTL)) { + started = true; + dist += charDist; + } else if (!started) { + dist += charDist; + } else { + break; + } + + text = remaining; + } + + return dist; + }; + /** + * Split a string in two parts at a given distance starting from the end when + * `isRTL` is set to `true`. + */ + + var splitByCharacterDistance = function splitByCharacterDistance(str, dist, isRTL) { + if (isRTL) { + var at = str.length - dist; + return [str.slice(at, str.length), str.slice(0, at)]; + } + + return [str.slice(0, dist), str.slice(dist)]; + }; + /** + * Check if a character is a word character. The `remaining` argument is used + * because sometimes you must read subsequent characters to truly determine it. + */ + + var isWordCharacter = function isWordCharacter(_char3, remaining) { + var isRTL = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + if (SPACE.test(_char3)) { + return false; + } // Chameleons count as word characters as long as they're in a word, so + // recurse to see if the next one is a word character or not. + + + if (CHAMELEON.test(_char3)) { + var charDist = getCharacterDistance(remaining, isRTL); + + var _splitByCharacterDist3 = splitByCharacterDistance(remaining, charDist, isRTL), + _splitByCharacterDist4 = _slicedToArray(_splitByCharacterDist3, 2), + nextChar = _splitByCharacterDist4[0], + nextRemaining = _splitByCharacterDist4[1]; + + if (isWordCharacter(nextChar, nextRemaining, isRTL)) { + return true; + } + } + + if (PUNCTUATION.test(_char3)) { + return false; + } + + return true; + }; + /** + * Iterate on codepoints from right to left. + */ + + + var codepointsIteratorRTL = function* codepointsIteratorRTL(str) { + var end = str.length - 1; + + for (var i = 0; i < str.length; i++) { + var char1 = str.charAt(end - i); + + if (isLowSurrogate(char1.charCodeAt(0))) { + var char2 = str.charAt(end - i - 1); + + if (isHighSurrogate(char2.charCodeAt(0))) { + yield char2 + char1; + i++; + continue; + } + } + + yield char1; + } + }; + /** + * Is `charCode` a high surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + + var isHighSurrogate = function isHighSurrogate(charCode) { + return charCode >= 0xd800 && charCode <= 0xdbff; + }; + /** + * Is `charCode` a low surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + + + var isLowSurrogate = function isLowSurrogate(charCode) { + return charCode >= 0xdc00 && charCode <= 0xdfff; + }; + + var CodepointType; + + (function (CodepointType) { + CodepointType[CodepointType["None"] = 0] = "None"; + CodepointType[CodepointType["Extend"] = 1] = "Extend"; + CodepointType[CodepointType["ZWJ"] = 2] = "ZWJ"; + CodepointType[CodepointType["RI"] = 4] = "RI"; + CodepointType[CodepointType["Prepend"] = 8] = "Prepend"; + CodepointType[CodepointType["SpacingMark"] = 16] = "SpacingMark"; + CodepointType[CodepointType["L"] = 32] = "L"; + CodepointType[CodepointType["V"] = 64] = "V"; + CodepointType[CodepointType["T"] = 128] = "T"; + CodepointType[CodepointType["LV"] = 256] = "LV"; + CodepointType[CodepointType["LVT"] = 512] = "LVT"; + CodepointType[CodepointType["ExtPict"] = 1024] = "ExtPict"; + CodepointType[CodepointType["Any"] = 2048] = "Any"; + })(CodepointType || (CodepointType = {})); + + var reExtend = /^(?:[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1AC0\u1B00-\u1B03\u1B34-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200C\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E\uFF9F]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD803[\uDD24-\uDD27\uDEAB\uDEAC\uDF46-\uDF50]|\uD804[\uDC01\uDC38-\uDC46\uDC7F-\uDC81\uDCB3-\uDCB6\uDCB9\uDCBA\uDD00-\uDD02\uDD27-\uDD2B\uDD2D-\uDD34\uDD73\uDD80\uDD81\uDDB6-\uDDBE\uDDC9-\uDDCC\uDDCF\uDE2F-\uDE31\uDE34\uDE36\uDE37\uDE3E\uDEDF\uDEE3-\uDEEA\uDF00\uDF01\uDF3B\uDF3C\uDF3E\uDF40\uDF57\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC38-\uDC3F\uDC42-\uDC44\uDC46\uDC5E\uDCB0\uDCB3-\uDCB8\uDCBA\uDCBD\uDCBF\uDCC0\uDCC2\uDCC3\uDDAF\uDDB2-\uDDB5\uDDBC\uDDBD\uDDBF\uDDC0\uDDDC\uDDDD\uDE33-\uDE3A\uDE3D\uDE3F\uDE40\uDEAB\uDEAD\uDEB0-\uDEB5\uDEB7\uDF1D-\uDF1F\uDF22-\uDF25\uDF27-\uDF2B]|\uD806[\uDC2F-\uDC37\uDC39\uDC3A\uDD30\uDD3B\uDD3C\uDD3E\uDD43\uDDD4-\uDDD7\uDDDA\uDDDB\uDDE0\uDE01-\uDE0A\uDE33-\uDE38\uDE3B-\uDE3E\uDE47\uDE51-\uDE56\uDE59-\uDE5B\uDE8A-\uDE96\uDE98\uDE99]|\uD807[\uDC30-\uDC36\uDC38-\uDC3D\uDC3F\uDC92-\uDCA7\uDCAA-\uDCB0\uDCB2\uDCB3\uDCB5\uDCB6\uDD31-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD45\uDD47\uDD90\uDD91\uDD95\uDD97\uDEF3\uDEF4]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF4F\uDF8F-\uDF92\uDFE4]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65\uDD67-\uDD69\uDD6E-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD30-\uDD36\uDEEC-\uDEEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uD83C[\uDFFB-\uDFFF]|\uDB40[\uDC20-\uDC7F\uDD00-\uDDEF])$/; + var rePrepend = /^(?:[\u0600-\u0605\u06DD\u070F\u0890\u0891\u08E2\u0D4E]|\uD804[\uDCBD\uDCCD\uDDC2\uDDC3]|\uD806[\uDD3F\uDD41\uDE3A\uDE84-\uDE89]|\uD807\uDD46)$/; + var reSpacingMark = /^(?:[\u0903\u093B\u093E-\u0940\u0949-\u094C\u094E\u094F\u0982\u0983\u09BF\u09C0\u09C7\u09C8\u09CB\u09CC\u0A03\u0A3E-\u0A40\u0A83\u0ABE-\u0AC0\u0AC9\u0ACB\u0ACC\u0B02\u0B03\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0C01-\u0C03\u0C41-\u0C44\u0C82\u0C83\u0CBE\u0CC0\u0CC1\u0CC3\u0CC4\u0CC7\u0CC8\u0CCA\u0CCB\u0D02\u0D03\u0D3F\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D82\u0D83\u0DD0\u0DD1\u0DD8-\u0DDE\u0DF2\u0DF3\u0E33\u0EB3\u0F3E\u0F3F\u0F7F\u1031\u103B\u103C\u1056\u1057\u1084\u1715\u1734\u17B6\u17BE-\u17C5\u17C7\u17C8\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1A19\u1A1A\u1A55\u1A57\u1A6D-\u1A72\u1B04\u1B3B\u1B3D-\u1B41\u1B43\u1B44\u1B82\u1BA1\u1BA6\u1BA7\u1BAA\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1C24-\u1C2B\u1C34\u1C35\u1CE1\u1CF7\uA823\uA824\uA827\uA880\uA881\uA8B4-\uA8C3\uA952\uA953\uA983\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9C0\uAA2F\uAA30\uAA33\uAA34\uAA4D\uAAEB\uAAEE\uAAEF\uAAF5\uABE3\uABE4\uABE6\uABE7\uABE9\uABEA\uABEC]|\uD804[\uDC00\uDC02\uDC82\uDCB0-\uDCB2\uDCB7\uDCB8\uDD2C\uDD45\uDD46\uDD82\uDDB3-\uDDB5\uDDBF\uDDC0\uDDCE\uDE2C-\uDE2E\uDE32\uDE33\uDE35\uDEE0-\uDEE2\uDF02\uDF03\uDF3F\uDF41-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF62\uDF63]|\uD805[\uDC35-\uDC37\uDC40\uDC41\uDC45\uDCB1\uDCB2\uDCB9\uDCBB\uDCBC\uDCBE\uDCC1\uDDB0\uDDB1\uDDB8-\uDDBB\uDDBE\uDE30-\uDE32\uDE3B\uDE3C\uDE3E\uDEAC\uDEAE\uDEAF\uDEB6\uDF26]|\uD806[\uDC2C-\uDC2E\uDC38\uDD31-\uDD35\uDD37\uDD38\uDD3D\uDD40\uDD42\uDDD1-\uDDD3\uDDDC-\uDDDF\uDDE4\uDE39\uDE57\uDE58\uDE97]|\uD807[\uDC2F\uDC3E\uDCA9\uDCB1\uDCB4\uDD8A-\uDD8E\uDD93\uDD94\uDD96\uDEF5\uDEF6]|\uD81B[\uDF51-\uDF87\uDFF0\uDFF1]|\uD834[\uDD66\uDD6D])$/; + var reL = /^[\u1100-\u115F\uA960-\uA97C]$/; + var reV = /^[\u1160-\u11A7\uD7B0-\uD7C6]$/; + var reT = /^[\u11A8-\u11FF\uD7CB-\uD7FB]$/; + var reLV = /^[\uAC00\uAC1C\uAC38\uAC54\uAC70\uAC8C\uACA8\uACC4\uACE0\uACFC\uAD18\uAD34\uAD50\uAD6C\uAD88\uADA4\uADC0\uADDC\uADF8\uAE14\uAE30\uAE4C\uAE68\uAE84\uAEA0\uAEBC\uAED8\uAEF4\uAF10\uAF2C\uAF48\uAF64\uAF80\uAF9C\uAFB8\uAFD4\uAFF0\uB00C\uB028\uB044\uB060\uB07C\uB098\uB0B4\uB0D0\uB0EC\uB108\uB124\uB140\uB15C\uB178\uB194\uB1B0\uB1CC\uB1E8\uB204\uB220\uB23C\uB258\uB274\uB290\uB2AC\uB2C8\uB2E4\uB300\uB31C\uB338\uB354\uB370\uB38C\uB3A8\uB3C4\uB3E0\uB3FC\uB418\uB434\uB450\uB46C\uB488\uB4A4\uB4C0\uB4DC\uB4F8\uB514\uB530\uB54C\uB568\uB584\uB5A0\uB5BC\uB5D8\uB5F4\uB610\uB62C\uB648\uB664\uB680\uB69C\uB6B8\uB6D4\uB6F0\uB70C\uB728\uB744\uB760\uB77C\uB798\uB7B4\uB7D0\uB7EC\uB808\uB824\uB840\uB85C\uB878\uB894\uB8B0\uB8CC\uB8E8\uB904\uB920\uB93C\uB958\uB974\uB990\uB9AC\uB9C8\uB9E4\uBA00\uBA1C\uBA38\uBA54\uBA70\uBA8C\uBAA8\uBAC4\uBAE0\uBAFC\uBB18\uBB34\uBB50\uBB6C\uBB88\uBBA4\uBBC0\uBBDC\uBBF8\uBC14\uBC30\uBC4C\uBC68\uBC84\uBCA0\uBCBC\uBCD8\uBCF4\uBD10\uBD2C\uBD48\uBD64\uBD80\uBD9C\uBDB8\uBDD4\uBDF0\uBE0C\uBE28\uBE44\uBE60\uBE7C\uBE98\uBEB4\uBED0\uBEEC\uBF08\uBF24\uBF40\uBF5C\uBF78\uBF94\uBFB0\uBFCC\uBFE8\uC004\uC020\uC03C\uC058\uC074\uC090\uC0AC\uC0C8\uC0E4\uC100\uC11C\uC138\uC154\uC170\uC18C\uC1A8\uC1C4\uC1E0\uC1FC\uC218\uC234\uC250\uC26C\uC288\uC2A4\uC2C0\uC2DC\uC2F8\uC314\uC330\uC34C\uC368\uC384\uC3A0\uC3BC\uC3D8\uC3F4\uC410\uC42C\uC448\uC464\uC480\uC49C\uC4B8\uC4D4\uC4F0\uC50C\uC528\uC544\uC560\uC57C\uC598\uC5B4\uC5D0\uC5EC\uC608\uC624\uC640\uC65C\uC678\uC694\uC6B0\uC6CC\uC6E8\uC704\uC720\uC73C\uC758\uC774\uC790\uC7AC\uC7C8\uC7E4\uC800\uC81C\uC838\uC854\uC870\uC88C\uC8A8\uC8C4\uC8E0\uC8FC\uC918\uC934\uC950\uC96C\uC988\uC9A4\uC9C0\uC9DC\uC9F8\uCA14\uCA30\uCA4C\uCA68\uCA84\uCAA0\uCABC\uCAD8\uCAF4\uCB10\uCB2C\uCB48\uCB64\uCB80\uCB9C\uCBB8\uCBD4\uCBF0\uCC0C\uCC28\uCC44\uCC60\uCC7C\uCC98\uCCB4\uCCD0\uCCEC\uCD08\uCD24\uCD40\uCD5C\uCD78\uCD94\uCDB0\uCDCC\uCDE8\uCE04\uCE20\uCE3C\uCE58\uCE74\uCE90\uCEAC\uCEC8\uCEE4\uCF00\uCF1C\uCF38\uCF54\uCF70\uCF8C\uCFA8\uCFC4\uCFE0\uCFFC\uD018\uD034\uD050\uD06C\uD088\uD0A4\uD0C0\uD0DC\uD0F8\uD114\uD130\uD14C\uD168\uD184\uD1A0\uD1BC\uD1D8\uD1F4\uD210\uD22C\uD248\uD264\uD280\uD29C\uD2B8\uD2D4\uD2F0\uD30C\uD328\uD344\uD360\uD37C\uD398\uD3B4\uD3D0\uD3EC\uD408\uD424\uD440\uD45C\uD478\uD494\uD4B0\uD4CC\uD4E8\uD504\uD520\uD53C\uD558\uD574\uD590\uD5AC\uD5C8\uD5E4\uD600\uD61C\uD638\uD654\uD670\uD68C\uD6A8\uD6C4\uD6E0\uD6FC\uD718\uD734\uD750\uD76C\uD788]$/; + var reLVT = /^[\uAC01-\uAC1B\uAC1D-\uAC37\uAC39-\uAC53\uAC55-\uAC6F\uAC71-\uAC8B\uAC8D-\uACA7\uACA9-\uACC3\uACC5-\uACDF\uACE1-\uACFB\uACFD-\uAD17\uAD19-\uAD33\uAD35-\uAD4F\uAD51-\uAD6B\uAD6D-\uAD87\uAD89-\uADA3\uADA5-\uADBF\uADC1-\uADDB\uADDD-\uADF7\uADF9-\uAE13\uAE15-\uAE2F\uAE31-\uAE4B\uAE4D-\uAE67\uAE69-\uAE83\uAE85-\uAE9F\uAEA1-\uAEBB\uAEBD-\uAED7\uAED9-\uAEF3\uAEF5-\uAF0F\uAF11-\uAF2B\uAF2D-\uAF47\uAF49-\uAF63\uAF65-\uAF7F\uAF81-\uAF9B\uAF9D-\uAFB7\uAFB9-\uAFD3\uAFD5-\uAFEF\uAFF1-\uB00B\uB00D-\uB027\uB029-\uB043\uB045-\uB05F\uB061-\uB07B\uB07D-\uB097\uB099-\uB0B3\uB0B5-\uB0CF\uB0D1-\uB0EB\uB0ED-\uB107\uB109-\uB123\uB125-\uB13F\uB141-\uB15B\uB15D-\uB177\uB179-\uB193\uB195-\uB1AF\uB1B1-\uB1CB\uB1CD-\uB1E7\uB1E9-\uB203\uB205-\uB21F\uB221-\uB23B\uB23D-\uB257\uB259-\uB273\uB275-\uB28F\uB291-\uB2AB\uB2AD-\uB2C7\uB2C9-\uB2E3\uB2E5-\uB2FF\uB301-\uB31B\uB31D-\uB337\uB339-\uB353\uB355-\uB36F\uB371-\uB38B\uB38D-\uB3A7\uB3A9-\uB3C3\uB3C5-\uB3DF\uB3E1-\uB3FB\uB3FD-\uB417\uB419-\uB433\uB435-\uB44F\uB451-\uB46B\uB46D-\uB487\uB489-\uB4A3\uB4A5-\uB4BF\uB4C1-\uB4DB\uB4DD-\uB4F7\uB4F9-\uB513\uB515-\uB52F\uB531-\uB54B\uB54D-\uB567\uB569-\uB583\uB585-\uB59F\uB5A1-\uB5BB\uB5BD-\uB5D7\uB5D9-\uB5F3\uB5F5-\uB60F\uB611-\uB62B\uB62D-\uB647\uB649-\uB663\uB665-\uB67F\uB681-\uB69B\uB69D-\uB6B7\uB6B9-\uB6D3\uB6D5-\uB6EF\uB6F1-\uB70B\uB70D-\uB727\uB729-\uB743\uB745-\uB75F\uB761-\uB77B\uB77D-\uB797\uB799-\uB7B3\uB7B5-\uB7CF\uB7D1-\uB7EB\uB7ED-\uB807\uB809-\uB823\uB825-\uB83F\uB841-\uB85B\uB85D-\uB877\uB879-\uB893\uB895-\uB8AF\uB8B1-\uB8CB\uB8CD-\uB8E7\uB8E9-\uB903\uB905-\uB91F\uB921-\uB93B\uB93D-\uB957\uB959-\uB973\uB975-\uB98F\uB991-\uB9AB\uB9AD-\uB9C7\uB9C9-\uB9E3\uB9E5-\uB9FF\uBA01-\uBA1B\uBA1D-\uBA37\uBA39-\uBA53\uBA55-\uBA6F\uBA71-\uBA8B\uBA8D-\uBAA7\uBAA9-\uBAC3\uBAC5-\uBADF\uBAE1-\uBAFB\uBAFD-\uBB17\uBB19-\uBB33\uBB35-\uBB4F\uBB51-\uBB6B\uBB6D-\uBB87\uBB89-\uBBA3\uBBA5-\uBBBF\uBBC1-\uBBDB\uBBDD-\uBBF7\uBBF9-\uBC13\uBC15-\uBC2F\uBC31-\uBC4B\uBC4D-\uBC67\uBC69-\uBC83\uBC85-\uBC9F\uBCA1-\uBCBB\uBCBD-\uBCD7\uBCD9-\uBCF3\uBCF5-\uBD0F\uBD11-\uBD2B\uBD2D-\uBD47\uBD49-\uBD63\uBD65-\uBD7F\uBD81-\uBD9B\uBD9D-\uBDB7\uBDB9-\uBDD3\uBDD5-\uBDEF\uBDF1-\uBE0B\uBE0D-\uBE27\uBE29-\uBE43\uBE45-\uBE5F\uBE61-\uBE7B\uBE7D-\uBE97\uBE99-\uBEB3\uBEB5-\uBECF\uBED1-\uBEEB\uBEED-\uBF07\uBF09-\uBF23\uBF25-\uBF3F\uBF41-\uBF5B\uBF5D-\uBF77\uBF79-\uBF93\uBF95-\uBFAF\uBFB1-\uBFCB\uBFCD-\uBFE7\uBFE9-\uC003\uC005-\uC01F\uC021-\uC03B\uC03D-\uC057\uC059-\uC073\uC075-\uC08F\uC091-\uC0AB\uC0AD-\uC0C7\uC0C9-\uC0E3\uC0E5-\uC0FF\uC101-\uC11B\uC11D-\uC137\uC139-\uC153\uC155-\uC16F\uC171-\uC18B\uC18D-\uC1A7\uC1A9-\uC1C3\uC1C5-\uC1DF\uC1E1-\uC1FB\uC1FD-\uC217\uC219-\uC233\uC235-\uC24F\uC251-\uC26B\uC26D-\uC287\uC289-\uC2A3\uC2A5-\uC2BF\uC2C1-\uC2DB\uC2DD-\uC2F7\uC2F9-\uC313\uC315-\uC32F\uC331-\uC34B\uC34D-\uC367\uC369-\uC383\uC385-\uC39F\uC3A1-\uC3BB\uC3BD-\uC3D7\uC3D9-\uC3F3\uC3F5-\uC40F\uC411-\uC42B\uC42D-\uC447\uC449-\uC463\uC465-\uC47F\uC481-\uC49B\uC49D-\uC4B7\uC4B9-\uC4D3\uC4D5-\uC4EF\uC4F1-\uC50B\uC50D-\uC527\uC529-\uC543\uC545-\uC55F\uC561-\uC57B\uC57D-\uC597\uC599-\uC5B3\uC5B5-\uC5CF\uC5D1-\uC5EB\uC5ED-\uC607\uC609-\uC623\uC625-\uC63F\uC641-\uC65B\uC65D-\uC677\uC679-\uC693\uC695-\uC6AF\uC6B1-\uC6CB\uC6CD-\uC6E7\uC6E9-\uC703\uC705-\uC71F\uC721-\uC73B\uC73D-\uC757\uC759-\uC773\uC775-\uC78F\uC791-\uC7AB\uC7AD-\uC7C7\uC7C9-\uC7E3\uC7E5-\uC7FF\uC801-\uC81B\uC81D-\uC837\uC839-\uC853\uC855-\uC86F\uC871-\uC88B\uC88D-\uC8A7\uC8A9-\uC8C3\uC8C5-\uC8DF\uC8E1-\uC8FB\uC8FD-\uC917\uC919-\uC933\uC935-\uC94F\uC951-\uC96B\uC96D-\uC987\uC989-\uC9A3\uC9A5-\uC9BF\uC9C1-\uC9DB\uC9DD-\uC9F7\uC9F9-\uCA13\uCA15-\uCA2F\uCA31-\uCA4B\uCA4D-\uCA67\uCA69-\uCA83\uCA85-\uCA9F\uCAA1-\uCABB\uCABD-\uCAD7\uCAD9-\uCAF3\uCAF5-\uCB0F\uCB11-\uCB2B\uCB2D-\uCB47\uCB49-\uCB63\uCB65-\uCB7F\uCB81-\uCB9B\uCB9D-\uCBB7\uCBB9-\uCBD3\uCBD5-\uCBEF\uCBF1-\uCC0B\uCC0D-\uCC27\uCC29-\uCC43\uCC45-\uCC5F\uCC61-\uCC7B\uCC7D-\uCC97\uCC99-\uCCB3\uCCB5-\uCCCF\uCCD1-\uCCEB\uCCED-\uCD07\uCD09-\uCD23\uCD25-\uCD3F\uCD41-\uCD5B\uCD5D-\uCD77\uCD79-\uCD93\uCD95-\uCDAF\uCDB1-\uCDCB\uCDCD-\uCDE7\uCDE9-\uCE03\uCE05-\uCE1F\uCE21-\uCE3B\uCE3D-\uCE57\uCE59-\uCE73\uCE75-\uCE8F\uCE91-\uCEAB\uCEAD-\uCEC7\uCEC9-\uCEE3\uCEE5-\uCEFF\uCF01-\uCF1B\uCF1D-\uCF37\uCF39-\uCF53\uCF55-\uCF6F\uCF71-\uCF8B\uCF8D-\uCFA7\uCFA9-\uCFC3\uCFC5-\uCFDF\uCFE1-\uCFFB\uCFFD-\uD017\uD019-\uD033\uD035-\uD04F\uD051-\uD06B\uD06D-\uD087\uD089-\uD0A3\uD0A5-\uD0BF\uD0C1-\uD0DB\uD0DD-\uD0F7\uD0F9-\uD113\uD115-\uD12F\uD131-\uD14B\uD14D-\uD167\uD169-\uD183\uD185-\uD19F\uD1A1-\uD1BB\uD1BD-\uD1D7\uD1D9-\uD1F3\uD1F5-\uD20F\uD211-\uD22B\uD22D-\uD247\uD249-\uD263\uD265-\uD27F\uD281-\uD29B\uD29D-\uD2B7\uD2B9-\uD2D3\uD2D5-\uD2EF\uD2F1-\uD30B\uD30D-\uD327\uD329-\uD343\uD345-\uD35F\uD361-\uD37B\uD37D-\uD397\uD399-\uD3B3\uD3B5-\uD3CF\uD3D1-\uD3EB\uD3ED-\uD407\uD409-\uD423\uD425-\uD43F\uD441-\uD45B\uD45D-\uD477\uD479-\uD493\uD495-\uD4AF\uD4B1-\uD4CB\uD4CD-\uD4E7\uD4E9-\uD503\uD505-\uD51F\uD521-\uD53B\uD53D-\uD557\uD559-\uD573\uD575-\uD58F\uD591-\uD5AB\uD5AD-\uD5C7\uD5C9-\uD5E3\uD5E5-\uD5FF\uD601-\uD61B\uD61D-\uD637\uD639-\uD653\uD655-\uD66F\uD671-\uD68B\uD68D-\uD6A7\uD6A9-\uD6C3\uD6C5-\uD6DF\uD6E1-\uD6FB\uD6FD-\uD717\uD719-\uD733\uD735-\uD74F\uD751-\uD76B\uD76D-\uD787\uD789-\uD7A3]$/; + var reExtPict = /^(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u2388\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2605\u2607-\u2612\u2614-\u2685\u2690-\u2705\u2708-\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2767\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC00-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAD-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDFFA]|\uD83D[\uDC00-\uDD3D\uDD46-\uDE4F\uDE80-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDEFF]|\uD83F[\uDC00-\uDFFD])$/; + + var getCodepointType = function getCodepointType(_char4, code) { + var type = CodepointType.Any; + + if (_char4.search(reExtend) !== -1) { + type |= CodepointType.Extend; + } + + if (code === 0x200d) { + type |= CodepointType.ZWJ; + } + + if (code >= 0x1f1e6 && code <= 0x1f1ff) { + type |= CodepointType.RI; + } + + if (_char4.search(rePrepend) !== -1) { + type |= CodepointType.Prepend; + } + + if (_char4.search(reSpacingMark) !== -1) { + type |= CodepointType.SpacingMark; + } + + if (_char4.search(reL) !== -1) { + type |= CodepointType.L; + } + + if (_char4.search(reV) !== -1) { + type |= CodepointType.V; + } + + if (_char4.search(reT) !== -1) { + type |= CodepointType.T; + } + + if (_char4.search(reLV) !== -1) { + type |= CodepointType.LV; + } + + if (_char4.search(reLVT) !== -1) { + type |= CodepointType.LVT; + } + + if (_char4.search(reExtPict) !== -1) { + type |= CodepointType.ExtPict; + } + + return type; + }; + + function intersects(x, y) { + return (x & y) !== 0; + } + + var NonBoundaryPairs = [// GB6 + [CodepointType.L, CodepointType.L | CodepointType.V | CodepointType.LV | CodepointType.LVT], // GB7 + [CodepointType.LV | CodepointType.V, CodepointType.V | CodepointType.T], // GB8 + [CodepointType.LVT | CodepointType.T, CodepointType.T], // GB9 + [CodepointType.Any, CodepointType.Extend | CodepointType.ZWJ], // GB9a + [CodepointType.Any, CodepointType.SpacingMark], // GB9b + [CodepointType.Prepend, CodepointType.Any], // GB11 + [CodepointType.ZWJ, CodepointType.ExtPict], // GB12 and GB13 + [CodepointType.RI, CodepointType.RI]]; + + function isBoundaryPair(left, right) { + return NonBoundaryPairs.findIndex(function (r) { + return intersects(left, r[0]) && intersects(right, r[1]); + }) === -1; + } + + var endingEmojiZWJ = /(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u2388\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2605\u2607-\u2612\u2614-\u2685\u2690-\u2705\u2708-\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2767\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC00-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAD-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDFFA]|\uD83D[\uDC00-\uDD3D\uDD46-\uDE4F\uDE80-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDEFF]|\uD83F[\uDC00-\uDFFD])(?:[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1AC0\u1B00-\u1B03\u1B34-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200C\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E\uFF9F]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD803[\uDD24-\uDD27\uDEAB\uDEAC\uDF46-\uDF50]|\uD804[\uDC01\uDC38-\uDC46\uDC7F-\uDC81\uDCB3-\uDCB6\uDCB9\uDCBA\uDD00-\uDD02\uDD27-\uDD2B\uDD2D-\uDD34\uDD73\uDD80\uDD81\uDDB6-\uDDBE\uDDC9-\uDDCC\uDDCF\uDE2F-\uDE31\uDE34\uDE36\uDE37\uDE3E\uDEDF\uDEE3-\uDEEA\uDF00\uDF01\uDF3B\uDF3C\uDF3E\uDF40\uDF57\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC38-\uDC3F\uDC42-\uDC44\uDC46\uDC5E\uDCB0\uDCB3-\uDCB8\uDCBA\uDCBD\uDCBF\uDCC0\uDCC2\uDCC3\uDDAF\uDDB2-\uDDB5\uDDBC\uDDBD\uDDBF\uDDC0\uDDDC\uDDDD\uDE33-\uDE3A\uDE3D\uDE3F\uDE40\uDEAB\uDEAD\uDEB0-\uDEB5\uDEB7\uDF1D-\uDF1F\uDF22-\uDF25\uDF27-\uDF2B]|\uD806[\uDC2F-\uDC37\uDC39\uDC3A\uDD30\uDD3B\uDD3C\uDD3E\uDD43\uDDD4-\uDDD7\uDDDA\uDDDB\uDDE0\uDE01-\uDE0A\uDE33-\uDE38\uDE3B-\uDE3E\uDE47\uDE51-\uDE56\uDE59-\uDE5B\uDE8A-\uDE96\uDE98\uDE99]|\uD807[\uDC30-\uDC36\uDC38-\uDC3D\uDC3F\uDC92-\uDCA7\uDCAA-\uDCB0\uDCB2\uDCB3\uDCB5\uDCB6\uDD31-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD45\uDD47\uDD90\uDD91\uDD95\uDD97\uDEF3\uDEF4]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF4F\uDF8F-\uDF92\uDFE4]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65\uDD67-\uDD69\uDD6E-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD30-\uDD36\uDEEC-\uDEEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uD83C[\uDFFB-\uDFFF]|\uDB40[\uDC20-\uDC7F\uDD00-\uDDEF])*\u200D$/; + + var endsWithEmojiZWJ = function endsWithEmojiZWJ(str) { + return str.search(endingEmojiZWJ) !== -1; + }; + + var endingRIs = /(?:\uD83C[\uDDE6-\uDDFF])+$/g; + + var endsWithOddNumberOfRIs = function endsWithOddNumberOfRIs(str) { + var match = str.match(endingRIs); + + if (match === null) { + return false; + } else { + // A RI is represented by a surrogate pair. + var numRIs = match[0].length / 2; + return numRIs % 2 === 1; + } + }; + + /** + * Shared the function with isElementType utility + */ + + var isElement = function isElement(value) { + return isPlainObject.isPlainObject(value) && Node$1.isNodeList(value.children) && !Editor.isEditor(value); + }; + + var Element$1 = { + /** + * Check if a value implements the 'Ancestor' interface. + */ + isAncestor: function isAncestor(value) { + return isPlainObject.isPlainObject(value) && Node$1.isNodeList(value.children); + }, + + /** + * Check if a value implements the `Element` interface. + */ + isElement: isElement, + + /** + * Check if a value is an array of `Element` objects. + */ + isElementList: function isElementList(value) { + return Array.isArray(value) && value.every(function (val) { + return Element$1.isElement(val); + }); + }, + + /** + * Check if a set of props is a partial of Element. + */ + isElementProps: function isElementProps(props) { + return props.children !== undefined; + }, + + /** + * Check if a value implements the `Element` interface and has elementKey with selected value. + * Default it check to `type` key value + */ + isElementType: function isElementType(value, elementVal) { + var elementKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'type'; + return isElement(value) && value[elementKey] === elementVal; + }, + + /** + * Check if an element matches set of properties. + * + * Note: this checks custom properties, and it does not ensure that any + * children are equivalent. + */ + matches: function matches(element, props) { + for (var key in props) { + if (key === 'children') { + continue; + } + + if (element[key] !== props[key]) { + return false; + } + } + + return true; + } + }; + + var _excluded$4 = ["text"], + _excluded2$3 = ["text"]; + + function ownKeys$8(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$8(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$8(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$8(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$5(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$5(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$5(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$5(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$5(o, minLen); } + + function _arrayLikeToArray$5(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var IS_EDITOR_CACHE = new WeakMap(); + var Editor = { + /** + * Get the ancestor above a location in the document. + */ + above: function above(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids, + _options$mode = options.mode, + mode = _options$mode === void 0 ? 'lowest' : _options$mode, + _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + match = options.match; + + if (!at) { + return; + } + + var path = Editor.path(editor, at); + var reverse = mode === 'lowest'; + + var _iterator = _createForOfIteratorHelper$5(Editor.levels(editor, { + at: path, + voids: voids, + match: match, + reverse: reverse + })), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _step$value = _slicedToArray(_step.value, 2), + n = _step$value[0], + p = _step$value[1]; + + if (!Text.isText(n) && !Path.equals(path, p)) { + return [n, p]; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + }, + + /** + * Add a custom property to the leaf text nodes in the current selection. + * + * If the selection is currently collapsed, the marks will be added to the + * `editor.marks` property instead, and applied when text is inserted next. + */ + addMark: function addMark(editor, key, value) { + editor.addMark(key, value); + }, + + /** + * Get the point after a location. + */ + after: function after(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var anchor = Editor.point(editor, at, { + edge: 'end' + }); + var focus = Editor.end(editor, []); + var range = { + anchor: anchor, + focus: focus + }; + var _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance; + var d = 0; + var target; + + var _iterator2 = _createForOfIteratorHelper$5(Editor.positions(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + at: range + }))), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var p = _step2.value; + + if (d > distance) { + break; + } + + if (d !== 0) { + target = p; + } + + d++; + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + return target; + }, + + /** + * Get the point before a location. + */ + before: function before(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var anchor = Editor.start(editor, []); + var focus = Editor.point(editor, at, { + edge: 'start' + }); + var range = { + anchor: anchor, + focus: focus + }; + var _options$distance2 = options.distance, + distance = _options$distance2 === void 0 ? 1 : _options$distance2; + var d = 0; + var target; + + var _iterator3 = _createForOfIteratorHelper$5(Editor.positions(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + at: range, + reverse: true + }))), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var p = _step3.value; + + if (d > distance) { + break; + } + + if (d !== 0) { + target = p; + } + + d++; + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + return target; + }, + + /** + * Delete content in the editor backward from the current selection. + */ + deleteBackward: function deleteBackward(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit; + editor.deleteBackward(unit); + }, + + /** + * Delete content in the editor forward from the current selection. + */ + deleteForward: function deleteForward(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$unit2 = options.unit, + unit = _options$unit2 === void 0 ? 'character' : _options$unit2; + editor.deleteForward(unit); + }, + + /** + * Delete the content in the current selection. + */ + deleteFragment: function deleteFragment(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$direction = options.direction, + direction = _options$direction === void 0 ? 'forward' : _options$direction; + editor.deleteFragment(direction); + }, + + /** + * Get the start and end points of a location. + */ + edges: function edges(editor, at) { + return [Editor.start(editor, at), Editor.end(editor, at)]; + }, + + /** + * Get the end point of a location. + */ + end: function end(editor, at) { + return Editor.point(editor, at, { + edge: 'end' + }); + }, + + /** + * Get the first node at a location. + */ + first: function first(editor, at) { + var path = Editor.path(editor, at, { + edge: 'start' + }); + return Editor.node(editor, path); + }, + + /** + * Get the fragment at a location. + */ + fragment: function fragment(editor, at) { + var range = Editor.range(editor, at); + var fragment = Node$1.fragment(editor, range); + return fragment; + }, + + /** + * Check if a node has block children. + */ + hasBlocks: function hasBlocks(editor, element) { + return element.children.some(function (n) { + return Editor.isBlock(editor, n); + }); + }, + + /** + * Check if a node has inline and text children. + */ + hasInlines: function hasInlines(editor, element) { + return element.children.some(function (n) { + return Text.isText(n) || Editor.isInline(editor, n); + }); + }, + + /** + * Check if a node has text children. + */ + hasTexts: function hasTexts(editor, element) { + return element.children.every(function (n) { + return Text.isText(n); + }); + }, + + /** + * Insert a block break at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertBreak: function insertBreak(editor) { + editor.insertBreak(); + }, + + /** + * Insert a fragment at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertFragment: function insertFragment(editor, fragment) { + editor.insertFragment(fragment); + }, + + /** + * Insert a node at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertNode: function insertNode(editor, node) { + editor.insertNode(node); + }, + + /** + * Insert text at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertText: function insertText(editor, text) { + editor.insertText(text); + }, + + /** + * Check if a value is a block `Element` object. + */ + isBlock: function isBlock(editor, value) { + return Element$1.isElement(value) && !editor.isInline(value); + }, + + /** + * Check if a value is an `Editor` object. + */ + isEditor: function isEditor(value) { + if (!isPlainObject.isPlainObject(value)) return false; + var cachedIsEditor = IS_EDITOR_CACHE.get(value); + + if (cachedIsEditor !== undefined) { + return cachedIsEditor; + } + + var isEditor = typeof value.addMark === 'function' && typeof value.apply === 'function' && typeof value.deleteBackward === 'function' && typeof value.deleteForward === 'function' && typeof value.deleteFragment === 'function' && typeof value.insertBreak === 'function' && typeof value.insertFragment === 'function' && typeof value.insertNode === 'function' && typeof value.insertText === 'function' && typeof value.isInline === 'function' && typeof value.isVoid === 'function' && typeof value.normalizeNode === 'function' && typeof value.onChange === 'function' && typeof value.removeMark === 'function' && (value.marks === null || isPlainObject.isPlainObject(value.marks)) && (value.selection === null || Range.isRange(value.selection)) && Node$1.isNodeList(value.children) && Operation.isOperationList(value.operations); + IS_EDITOR_CACHE.set(value, isEditor); + return isEditor; + }, + + /** + * Check if a point is the end point of a location. + */ + isEnd: function isEnd(editor, point, at) { + var end = Editor.end(editor, at); + return Point.equals(point, end); + }, + + /** + * Check if a point is an edge of a location. + */ + isEdge: function isEdge(editor, point, at) { + return Editor.isStart(editor, point, at) || Editor.isEnd(editor, point, at); + }, + + /** + * Check if an element is empty, accounting for void nodes. + */ + isEmpty: function isEmpty(editor, element) { + var children = element.children; + + var _children = _slicedToArray(children, 1), + first = _children[0]; + + return children.length === 0 || children.length === 1 && Text.isText(first) && first.text === '' && !editor.isVoid(element); + }, + + /** + * Check if a value is an inline `Element` object. + */ + isInline: function isInline(editor, value) { + return Element$1.isElement(value) && editor.isInline(value); + }, + + /** + * Check if the editor is currently normalizing after each operation. + */ + isNormalizing: function isNormalizing(editor) { + var isNormalizing = NORMALIZING.get(editor); + return isNormalizing === undefined ? true : isNormalizing; + }, + + /** + * Check if a point is the start point of a location. + */ + isStart: function isStart(editor, point, at) { + // PERF: If the offset isn't `0` we know it's not the start. + if (point.offset !== 0) { + return false; + } + + var start = Editor.start(editor, at); + return Point.equals(point, start); + }, + + /** + * Check if a value is a void `Element` object. + */ + isVoid: function isVoid(editor, value) { + return Element$1.isElement(value) && editor.isVoid(value); + }, + + /** + * Get the last node at a location. + */ + last: function last(editor, at) { + var path = Editor.path(editor, at, { + edge: 'end' + }); + return Editor.node(editor, path); + }, + + /** + * Get the leaf text node at a location. + */ + leaf: function leaf(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var node = Node$1.leaf(editor, path); + return [node, path]; + }, + + /** + * Iterate through all of the levels at a location. + */ + levels: function* levels(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2, + _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var match = options.match; + + if (match == null) { + match = function match() { + return true; + }; + } + + if (!at) { + return; + } + + var levels = []; + var path = Editor.path(editor, at); + + var _iterator4 = _createForOfIteratorHelper$5(Node$1.levels(editor, path)), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + n = _step4$value[0], + p = _step4$value[1]; + + if (!match(n, p)) { + continue; + } + + levels.push([n, p]); + + if (!voids && Editor.isVoid(editor, n)) { + break; + } + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + if (reverse) { + levels.reverse(); + } + + yield* levels; + }, + + /** + * Get the marks that would be added to text at the current selection. + */ + marks: function marks(editor) { + var marks = editor.marks, + selection = editor.selection; + + if (!selection) { + return null; + } + + if (marks) { + return marks; + } + + if (Range.isExpanded(selection)) { + var _Editor$nodes = Editor.nodes(editor, { + match: Text.isText + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + match = _Editor$nodes2[0]; + + if (match) { + var _match = _slicedToArray(match, 1), + _node = _match[0]; + + _node.text; + var _rest = _objectWithoutProperties(_node, _excluded$4); + + return _rest; + } else { + return {}; + } + } + + var anchor = selection.anchor; + var path = anchor.path; + + var _Editor$leaf = Editor.leaf(editor, path), + _Editor$leaf2 = _slicedToArray(_Editor$leaf, 1), + node = _Editor$leaf2[0]; + + if (anchor.offset === 0) { + var prev = Editor.previous(editor, { + at: path, + match: Text.isText + }); + var block = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + } + }); + + if (prev && block) { + var _prev = _slicedToArray(prev, 2), + prevNode = _prev[0], + prevPath = _prev[1]; + + var _block = _slicedToArray(block, 2), + blockPath = _block[1]; + + if (Path.isAncestor(blockPath, prevPath)) { + node = prevNode; + } + } + } + + var _node2 = node; + _node2.text; + var rest = _objectWithoutProperties(_node2, _excluded2$3); + + return rest; + }, + + /** + * Get the matching node in the branch of the document after a location. + */ + next: function next(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$mode2 = options.mode, + mode = _options$mode2 === void 0 ? 'lowest' : _options$mode2, + _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3; + var match = options.match, + _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3; + + if (!at) { + return; + } + + var pointAfterLocation = Editor.after(editor, at, { + voids: voids + }); + if (!pointAfterLocation) return; + + var _Editor$last = Editor.last(editor, []), + _Editor$last2 = _slicedToArray(_Editor$last, 2), + to = _Editor$last2[1]; + + var span = [pointAfterLocation.path, to]; + + if (Path.isPath(at) && at.length === 0) { + throw new Error("Cannot get the next node from the root node!"); + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent = Editor.parent(editor, at), + _Editor$parent2 = _slicedToArray(_Editor$parent, 1), + parent = _Editor$parent2[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match() { + return true; + }; + } + } + + var _Editor$nodes3 = Editor.nodes(editor, { + at: span, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes4 = _slicedToArray(_Editor$nodes3, 1), + next = _Editor$nodes4[0]; + + return next; + }, + + /** + * Get the node at a location. + */ + node: function node(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var node = Node$1.get(editor, path); + return [node, path]; + }, + + /** + * Iterate through all of the nodes in the Editor. + */ + nodes: function* nodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at4 = options.at, + at = _options$at4 === void 0 ? editor.selection : _options$at4, + _options$mode3 = options.mode, + mode = _options$mode3 === void 0 ? 'all' : _options$mode3, + _options$universal = options.universal, + universal = _options$universal === void 0 ? false : _options$universal, + _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2, + _options$voids4 = options.voids, + voids = _options$voids4 === void 0 ? false : _options$voids4; + var match = options.match; + + if (!match) { + match = function match() { + return true; + }; + } + + if (!at) { + return; + } + + var from; + var to; + + if (Span.isSpan(at)) { + from = at[0]; + to = at[1]; + } else { + var first = Editor.path(editor, at, { + edge: 'start' + }); + var last = Editor.path(editor, at, { + edge: 'end' + }); + from = reverse ? last : first; + to = reverse ? first : last; + } + + var nodeEntries = Node$1.nodes(editor, { + reverse: reverse, + from: from, + to: to, + pass: function pass(_ref) { + var _ref2 = _slicedToArray(_ref, 1), + n = _ref2[0]; + + return voids ? false : Editor.isVoid(editor, n); + } + }); + var matches = []; + var hit; + + var _iterator5 = _createForOfIteratorHelper$5(nodeEntries), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _step5$value = _slicedToArray(_step5.value, 2), + node = _step5$value[0], + path = _step5$value[1]; + + var isLower = hit && Path.compare(path, hit[1]) === 0; // In highest mode any node lower than the last hit is not a match. + + if (mode === 'highest' && isLower) { + continue; + } + + if (!match(node, path)) { + // If we've arrived at a leaf text node that is not lower than the last + // hit, then we've found a branch that doesn't include a match, which + // means the match is not universal. + if (universal && !isLower && Text.isText(node)) { + return; + } else { + continue; + } + } // If there's a match and it's lower than the last, update the hit. + + + if (mode === 'lowest' && isLower) { + hit = [node, path]; + continue; + } // In lowest mode we emit the last hit, once it's guaranteed lowest. + + + var emit = mode === 'lowest' ? hit : [node, path]; + + if (emit) { + if (universal) { + matches.push(emit); + } else { + yield emit; + } + } + + hit = [node, path]; + } // Since lowest is always emitting one behind, catch up at the end. + + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + if (mode === 'lowest' && hit) { + if (universal) { + matches.push(hit); + } else { + yield hit; + } + } // Universal defers to ensure that the match occurs in every branch, so we + // yield all of the matches after iterating. + + + if (universal) { + yield* matches; + } + }, + + /** + * Normalize any dirty objects in the editor. + */ + normalize: function normalize(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$force = options.force, + force = _options$force === void 0 ? false : _options$force; + + var getDirtyPaths = function getDirtyPaths(editor) { + return DIRTY_PATHS.get(editor) || []; + }; + + if (!Editor.isNormalizing(editor)) { + return; + } + + if (force) { + var allPaths = Array.from(Node$1.nodes(editor), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + p = _ref4[1]; + + return p; + }); + DIRTY_PATHS.set(editor, allPaths); + } + + if (getDirtyPaths(editor).length === 0) { + return; + } + + Editor.withoutNormalizing(editor, function () { + /* + Fix dirty elements with no children. + editor.normalizeNode() does fix this, but some normalization fixes also require it to work. + Running an initial pass avoids the catch-22 race condition. + */ + var _iterator6 = _createForOfIteratorHelper$5(getDirtyPaths(editor)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _dirtyPath = _step6.value; + + if (Node$1.has(editor, _dirtyPath)) { + var _entry = Editor.node(editor, _dirtyPath); + + var _entry2 = _slicedToArray(_entry, 2), + node = _entry2[0], + _ = _entry2[1]; + /* + The default normalizer inserts an empty text node in this scenario, but it can be customised. + So there is some risk here. + As long as the normalizer only inserts child nodes for this case it is safe to do in any order; + by definition adding children to an empty node can't cause other paths to change. + */ + + + if (Element$1.isElement(node) && node.children.length === 0) { + editor.normalizeNode(_entry); + } + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var max = getDirtyPaths(editor).length * 42; // HACK: better way? + + var m = 0; + + while (getDirtyPaths(editor).length !== 0) { + if (m > max) { + throw new Error("\n Could not completely normalize the editor after ".concat(max, " iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.\n ")); + } + + var dirtyPath = getDirtyPaths(editor).pop(); // If the node doesn't exist in the tree, it does not need to be normalized. + + if (Node$1.has(editor, dirtyPath)) { + var entry = Editor.node(editor, dirtyPath); + editor.normalizeNode(entry); + } + + m++; + } + }); + }, + + /** + * Get the parent node of a location. + */ + parent: function parent(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var parentPath = Path.parent(path); + var entry = Editor.node(editor, parentPath); + return entry; + }, + + /** + * Get the path of a location. + */ + path: function path(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var depth = options.depth, + edge = options.edge; + + if (Path.isPath(at)) { + if (edge === 'start') { + var _Node$first = Node$1.first(editor, at), + _Node$first2 = _slicedToArray(_Node$first, 2), + firstPath = _Node$first2[1]; + + at = firstPath; + } else if (edge === 'end') { + var _Node$last = Node$1.last(editor, at), + _Node$last2 = _slicedToArray(_Node$last, 2), + lastPath = _Node$last2[1]; + + at = lastPath; + } + } + + if (Range.isRange(at)) { + if (edge === 'start') { + at = Range.start(at); + } else if (edge === 'end') { + at = Range.end(at); + } else { + at = Path.common(at.anchor.path, at.focus.path); + } + } + + if (Point.isPoint(at)) { + at = at.path; + } + + if (depth != null) { + at = at.slice(0, depth); + } + + return at; + }, + hasPath: function hasPath(editor, path) { + return Node$1.has(editor, path); + }, + + /** + * Create a mutable ref for a `Path` object, which will stay in sync as new + * operations are applied to the editor. + */ + pathRef: function pathRef(editor, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; + var ref = { + current: path, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var pathRefs = Editor.pathRefs(editor); + pathRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.pathRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked path refs of the editor. + */ + pathRefs: function pathRefs(editor) { + var refs = PATH_REFS.get(editor); + + if (!refs) { + refs = new Set(); + PATH_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Get the start or end point of a location. + */ + point: function point(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$edge = options.edge, + edge = _options$edge === void 0 ? 'start' : _options$edge; + + if (Path.isPath(at)) { + var path; + + if (edge === 'end') { + var _Node$last3 = Node$1.last(editor, at), + _Node$last4 = _slicedToArray(_Node$last3, 2), + lastPath = _Node$last4[1]; + + path = lastPath; + } else { + var _Node$first3 = Node$1.first(editor, at), + _Node$first4 = _slicedToArray(_Node$first3, 2), + firstPath = _Node$first4[1]; + + path = firstPath; + } + + var node = Node$1.get(editor, path); + + if (!Text.isText(node)) { + throw new Error("Cannot get the ".concat(edge, " point in the node at path [").concat(at, "] because it has no ").concat(edge, " text node.")); + } + + return { + path: path, + offset: edge === 'end' ? node.text.length : 0 + }; + } + + if (Range.isRange(at)) { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + return edge === 'start' ? start : end; + } + + return at; + }, + + /** + * Create a mutable ref for a `Point` object, which will stay in sync as new + * operations are applied to the editor. + */ + pointRef: function pointRef(editor, point) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity2 = options.affinity, + affinity = _options$affinity2 === void 0 ? 'forward' : _options$affinity2; + var ref = { + current: point, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var pointRefs = Editor.pointRefs(editor); + pointRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.pointRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked point refs of the editor. + */ + pointRefs: function pointRefs(editor) { + var refs = POINT_REFS.get(editor); + + if (!refs) { + refs = new Set(); + POINT_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Return all the positions in `at` range where a `Point` can be placed. + * + * By default, moves forward by individual offsets at a time, but + * the `unit` option can be used to to move by character, word, line, or block. + * + * The `reverse` option can be used to change iteration direction. + * + * Note: By default void nodes are treated as a single point and iteration + * will not happen inside their content unless you pass in true for the + * `voids` option, then iteration will occur. + */ + positions: function* positions(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at5 = options.at, + at = _options$at5 === void 0 ? editor.selection : _options$at5, + _options$unit3 = options.unit, + unit = _options$unit3 === void 0 ? 'offset' : _options$unit3, + _options$reverse3 = options.reverse, + reverse = _options$reverse3 === void 0 ? false : _options$reverse3, + _options$voids5 = options.voids, + voids = _options$voids5 === void 0 ? false : _options$voids5; + + if (!at) { + return; + } + /** + * Algorithm notes: + * + * Each step `distance` is dynamic depending on the underlying text + * and the `unit` specified. Each step, e.g., a line or word, may + * span multiple text nodes, so we iterate through the text both on + * two levels in step-sync: + * + * `leafText` stores the text on a text leaf level, and is advanced + * through using the counters `leafTextOffset` and `leafTextRemaining`. + * + * `blockText` stores the text on a block level, and is shortened + * by `distance` every time it is advanced. + * + * We only maintain a window of one blockText and one leafText because + * a block node always appears before all of its leaf nodes. + */ + + + var range = Editor.range(editor, at); + + var _Range$edges3 = Range.edges(range), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + start = _Range$edges4[0], + end = _Range$edges4[1]; + + var first = reverse ? end : start; + var isNewBlock = false; + var blockText = ''; + var distance = 0; // Distance for leafText to catch up to blockText. + + var leafTextRemaining = 0; + var leafTextOffset = 0; // Iterate through all nodes in range, grabbing entire textual content + // of block nodes in blockText, and text nodes in leafText. + // Exploits the fact that nodes are sequenced in such a way that we first + // encounter the block node, then all of its text nodes, so when iterating + // through the blockText and leafText we just need to remember a window of + // one block node and leaf node, respectively. + + var _iterator7 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: at, + reverse: reverse, + voids: voids + })), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _step7$value = _slicedToArray(_step7.value, 2), + node = _step7$value[0], + path = _step7$value[1]; + + /* + * ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks + */ + if (Element$1.isElement(node)) { + // Void nodes are a special case, so by default we will always + // yield their first point. If the `voids` option is set to true, + // then we will iterate over their content. + if (!voids && editor.isVoid(node)) { + yield Editor.start(editor, path); + continue; + } // Inline element nodes are ignored as they don't themselves + // contribute to `blockText` or `leafText` - their parent and + // children do. + + + if (editor.isInline(node)) continue; // Block element node - set `blockText` to its text content. + + if (Editor.hasInlines(editor, node)) { + // We always exhaust block nodes before encountering a new one: + // console.assert(blockText === '', + // `blockText='${blockText}' - `+ + // `not exhausted before new block node`, path) + // Ensure range considered is capped to `range`, in the + // start/end edge cases where block extends beyond range. + // Equivalent to this, but presumably more performant: + // blockRange = Editor.range(editor, ...Editor.edges(editor, path)) + // blockRange = Range.intersection(range, blockRange) // intersect + // blockText = Editor.string(editor, blockRange, { voids }) + var e = Path.isAncestor(path, end.path) ? end : Editor.end(editor, path); + var s = Path.isAncestor(path, start.path) ? start : Editor.start(editor, path); + blockText = Editor.string(editor, { + anchor: s, + focus: e + }, { + voids: voids + }); + isNewBlock = true; + } + } + /* + * TEXT LEAF NODE - Iterate through text content, yielding + * positions every `distance` offset according to `unit`. + */ + + + if (Text.isText(node)) { + var isFirst = Path.equals(path, first.path); // Proof that we always exhaust text nodes before encountering a new one: + // console.assert(leafTextRemaining <= 0, + // `leafTextRemaining=${leafTextRemaining} - `+ + // `not exhausted before new leaf text node`, path) + // Reset `leafText` counters for new text node. + + if (isFirst) { + leafTextRemaining = reverse ? first.offset : node.text.length - first.offset; + leafTextOffset = first.offset; // Works for reverse too. + } else { + leafTextRemaining = node.text.length; + leafTextOffset = reverse ? leafTextRemaining : 0; + } // Yield position at the start of node (potentially). + + + if (isFirst || isNewBlock || unit === 'offset') { + yield { + path: path, + offset: leafTextOffset + }; + isNewBlock = false; + } // Yield positions every (dynamically calculated) `distance` offset. + + + while (true) { + // If `leafText` has caught up with `blockText` (distance=0), + // and if blockText is exhausted, break to get another block node, + // otherwise advance blockText forward by the new `distance`. + if (distance === 0) { + if (blockText === '') break; + distance = calcDistance(blockText, unit, reverse); // Split the string at the previously found distance and use the + // remaining string for the next iteration. + + blockText = splitByCharacterDistance(blockText, distance, reverse)[1]; + } // Advance `leafText` by the current `distance`. + + + leafTextOffset = reverse ? leafTextOffset - distance : leafTextOffset + distance; + leafTextRemaining = leafTextRemaining - distance; // If `leafText` is exhausted, break to get a new leaf node + // and set distance to the overflow amount, so we'll (maybe) + // catch up to blockText in the next leaf text node. + + if (leafTextRemaining < 0) { + distance = -leafTextRemaining; + break; + } // Successfully walked `distance` offsets through `leafText` + // to catch up with `blockText`, so we can reset `distance` + // and yield this position in this node. + + + distance = 0; + yield { + path: path, + offset: leafTextOffset + }; + } + } + } // Proof that upon completion, we've exahusted both leaf and block text: + // console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted") + // console.assert(blockText === '', "blockText wasn't exhausted") + // Helper: + // Return the distance in offsets for a step of size `unit` on given string. + + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + + function calcDistance(text, unit, reverse) { + if (unit === 'character') { + return getCharacterDistance(text, reverse); + } else if (unit === 'word') { + return getWordDistance(text, reverse); + } else if (unit === 'line' || unit === 'block') { + return text.length; + } + + return 1; + } + }, + + /** + * Get the matching node in the branch of the document before a location. + */ + previous: function previous(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$mode4 = options.mode, + mode = _options$mode4 === void 0 ? 'lowest' : _options$mode4, + _options$voids6 = options.voids, + voids = _options$voids6 === void 0 ? false : _options$voids6; + var match = options.match, + _options$at6 = options.at, + at = _options$at6 === void 0 ? editor.selection : _options$at6; + + if (!at) { + return; + } + + var pointBeforeLocation = Editor.before(editor, at, { + voids: voids + }); + + if (!pointBeforeLocation) { + return; + } + + var _Editor$first = Editor.first(editor, []), + _Editor$first2 = _slicedToArray(_Editor$first, 2), + to = _Editor$first2[1]; // The search location is from the start of the document to the path of + // the point before the location passed in + + + var span = [pointBeforeLocation.path, to]; + + if (Path.isPath(at) && at.length === 0) { + throw new Error("Cannot get the previous node from the root node!"); + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent3 = Editor.parent(editor, at), + _Editor$parent4 = _slicedToArray(_Editor$parent3, 1), + parent = _Editor$parent4[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match() { + return true; + }; + } + } + + var _Editor$nodes5 = Editor.nodes(editor, { + reverse: true, + at: span, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes6 = _slicedToArray(_Editor$nodes5, 1), + previous = _Editor$nodes6[0]; + + return previous; + }, + + /** + * Get a range of a location. + */ + range: function range(editor, at, to) { + if (Range.isRange(at) && !to) { + return at; + } + + var start = Editor.start(editor, at); + var end = Editor.end(editor, to || at); + return { + anchor: start, + focus: end + }; + }, + + /** + * Create a mutable ref for a `Range` object, which will stay in sync as new + * operations are applied to the editor. + */ + rangeRef: function rangeRef(editor, range) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity3 = options.affinity, + affinity = _options$affinity3 === void 0 ? 'forward' : _options$affinity3; + var ref = { + current: range, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var rangeRefs = Editor.rangeRefs(editor); + rangeRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.rangeRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked range refs of the editor. + */ + rangeRefs: function rangeRefs(editor) { + var refs = RANGE_REFS.get(editor); + + if (!refs) { + refs = new Set(); + RANGE_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Remove a custom property from all of the leaf text nodes in the current + * selection. + * + * If the selection is currently collapsed, the removal will be stored on + * `editor.marks` and applied to the text inserted next. + */ + removeMark: function removeMark(editor, key) { + editor.removeMark(key); + }, + + /** + * Manually set if the editor should currently be normalizing. + * + * Note: Using this incorrectly can leave the editor in an invalid state. + * + */ + setNormalizing: function setNormalizing(editor, isNormalizing) { + NORMALIZING.set(editor, isNormalizing); + }, + + /** + * Get the start point of a location. + */ + start: function start(editor, at) { + return Editor.point(editor, at, { + edge: 'start' + }); + }, + + /** + * Get the text string content of a location. + * + * Note: by default the text of void nodes is considered to be an empty + * string, regardless of content, unless you pass in true for the voids option + */ + string: function string(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$voids7 = options.voids, + voids = _options$voids7 === void 0 ? false : _options$voids7; + var range = Editor.range(editor, at); + + var _Range$edges5 = Range.edges(range), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + start = _Range$edges6[0], + end = _Range$edges6[1]; + + var text = ''; + + var _iterator8 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: range, + match: Text.isText, + voids: voids + })), + _step8; + + try { + for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { + var _step8$value = _slicedToArray(_step8.value, 2), + node = _step8$value[0], + path = _step8$value[1]; + + var t = node.text; + + if (Path.equals(path, end.path)) { + t = t.slice(0, end.offset); + } + + if (Path.equals(path, start.path)) { + t = t.slice(start.offset); + } + + text += t; + } + } catch (err) { + _iterator8.e(err); + } finally { + _iterator8.f(); + } + + return text; + }, + + /** + * Convert a range into a non-hanging one. + */ + unhangRange: function unhangRange(editor, range) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$voids8 = options.voids, + voids = _options$voids8 === void 0 ? false : _options$voids8; + + var _Range$edges7 = Range.edges(range), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; // PERF: exit early if we can guarantee that the range isn't hanging. + + + if (start.offset !== 0 || end.offset !== 0 || Range.isCollapsed(range)) { + return range; + } + + var endBlock = Editor.above(editor, { + at: end, + match: function match(n) { + return Editor.isBlock(editor, n); + } + }); + var blockPath = endBlock ? endBlock[1] : []; + var first = Editor.start(editor, []); + var before = { + anchor: first, + focus: end + }; + var skip = true; + + var _iterator9 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: before, + match: Text.isText, + reverse: true, + voids: voids + })), + _step9; + + try { + for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) { + var _step9$value = _slicedToArray(_step9.value, 2), + node = _step9$value[0], + path = _step9$value[1]; + + if (skip) { + skip = false; + continue; + } + + if (node.text !== '' || Path.isBefore(path, blockPath)) { + end = { + path: path, + offset: node.text.length + }; + break; + } + } + } catch (err) { + _iterator9.e(err); + } finally { + _iterator9.f(); + } + + return { + anchor: start, + focus: end + }; + }, + + /** + * Match a void node in the current branch of the editor. + */ + "void": function _void(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return Editor.above(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + match: function match(n) { + return Editor.isVoid(editor, n); + } + })); + }, + + /** + * Call a function, deferring normalization until after it completes. + */ + withoutNormalizing: function withoutNormalizing(editor, fn) { + var value = Editor.isNormalizing(editor); + Editor.setNormalizing(editor, false); + + try { + fn(); + } finally { + Editor.setNormalizing(editor, value); + } + + Editor.normalize(editor); + } + }; + + var Location = { + /** + * Check if a value implements the `Location` interface. + */ + isLocation: function isLocation(value) { + return Path.isPath(value) || Point.isPoint(value) || Range.isRange(value); + } + }; + var Span = { + /** + * Check if a value implements the `Span` interface. + */ + isSpan: function isSpan(value) { + return Array.isArray(value) && value.length === 2 && value.every(Path.isPath); + } + }; + + var _excluded$3 = ["children"], + _excluded2$2 = ["text"]; + + function _createForOfIteratorHelper$4(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$4(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$4(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$4(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$4(o, minLen); } + + function _arrayLikeToArray$4(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var IS_NODE_LIST_CACHE = new WeakMap(); + var Node$1 = { + /** + * Get the node at a specific path, asserting that it's an ancestor node. + */ + ancestor: function ancestor(root, path) { + var node = Node$1.get(root, path); + + if (Text.isText(node)) { + throw new Error("Cannot get the ancestor node at path [".concat(path, "] because it refers to a text node instead: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of all the ancestor nodes above a specific path. + * + * By default the order is bottom-up, from lowest to highest ancestor in + * the tree, but you can pass the `reverse: true` option to go top-down. + */ + ancestors: function* ancestors(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var _iterator = _createForOfIteratorHelper$4(Path.ancestors(path, options)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var p = _step.value; + var n = Node$1.ancestor(root, p); + var entry = [n, p]; + yield entry; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + }, + + /** + * Get the child of a node at a specific index. + */ + child: function child(root, index) { + if (Text.isText(root)) { + throw new Error("Cannot get the child of a text node: ".concat(JSON.stringify(root))); + } + + var c = root.children[index]; + + if (c == null) { + throw new Error("Cannot get child at index `".concat(index, "` in node: ").concat(JSON.stringify(root))); + } + + return c; + }, + + /** + * Iterate over the children of a node at a specific path. + */ + children: function* children(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var ancestor = Node$1.ancestor(root, path); + var children = ancestor.children; + var index = reverse ? children.length - 1 : 0; + + while (reverse ? index >= 0 : index < children.length) { + var child = Node$1.child(ancestor, index); + var childPath = path.concat(index); + yield [child, childPath]; + index = reverse ? index - 1 : index + 1; + } + }, + + /** + * Get an entry for the common ancesetor node of two paths. + */ + common: function common(root, path, another) { + var p = Path.common(path, another); + var n = Node$1.get(root, p); + return [n, p]; + }, + + /** + * Get the node at a specific path, asserting that it's a descendant node. + */ + descendant: function descendant(root, path) { + var node = Node$1.get(root, path); + + if (Editor.isEditor(node)) { + throw new Error("Cannot get the descendant node at path [".concat(path, "] because it refers to the root editor node instead: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of all the descendant node entries inside a root node. + */ + descendants: function* descendants(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator2 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + node = _step2$value[0], + path = _step2$value[1]; + + if (path.length !== 0) { + // NOTE: we have to coerce here because checking the path's length does + // guarantee that `node` is not a `Editor`, but TypeScript doesn't know. + yield [node, path]; + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + }, + + /** + * Return a generator of all the element nodes inside a root node. Each iteration + * will return an `ElementEntry` tuple consisting of `[Element, Path]`. If the + * root node is an element it will be included in the iteration as well. + */ + elements: function* elements(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator3 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + node = _step3$value[0], + path = _step3$value[1]; + + if (Element$1.isElement(node)) { + yield [node, path]; + } + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + }, + + /** + * Extract props from a Node. + */ + extractProps: function extractProps(node) { + if (Element$1.isAncestor(node)) { + node.children; + var properties = _objectWithoutProperties(node, _excluded$3); + + return properties; + } else { + node.text; + var _properties = _objectWithoutProperties(node, _excluded2$2); + + return _properties; + } + }, + + /** + * Get the first node entry in a root node from a path. + */ + first: function first(root, path) { + var p = path.slice(); + var n = Node$1.get(root, p); + + while (n) { + if (Text.isText(n) || n.children.length === 0) { + break; + } else { + n = n.children[0]; + p.push(0); + } + } + + return [n, p]; + }, + + /** + * Get the sliced fragment represented by a range inside a root node. + */ + fragment: function fragment(root, range) { + if (Text.isText(root)) { + throw new Error("Cannot get a fragment starting from a root text node: ".concat(JSON.stringify(root))); + } + + var newRoot = immer.produce({ + children: root.children + }, function (r) { + var _Range$edges = Range.edges(range), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + var nodeEntries = Node$1.nodes(r, { + reverse: true, + pass: function pass(_ref) { + var _ref2 = _slicedToArray(_ref, 2), + path = _ref2[1]; + + return !Range.includes(range, path); + } + }); + + var _iterator4 = _createForOfIteratorHelper$4(nodeEntries), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + path = _step4$value[1]; + + if (!Range.includes(range, path)) { + var parent = Node$1.parent(r, path); + var index = path[path.length - 1]; + parent.children.splice(index, 1); + } + + if (Path.equals(path, end.path)) { + var leaf = Node$1.leaf(r, path); + leaf.text = leaf.text.slice(0, end.offset); + } + + if (Path.equals(path, start.path)) { + var _leaf = Node$1.leaf(r, path); + + _leaf.text = _leaf.text.slice(start.offset); + } + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + if (Editor.isEditor(r)) { + r.selection = null; + } + }); + return newRoot.children; + }, + + /** + * Get the descendant node referred to by a specific path. If the path is an + * empty array, it refers to the root node itself. + */ + get: function get(root, path) { + var node = root; + + for (var i = 0; i < path.length; i++) { + var p = path[i]; + + if (Text.isText(node) || !node.children[p]) { + throw new Error("Cannot find a descendant at path [".concat(path, "] in node: ").concat(JSON.stringify(root))); + } + + node = node.children[p]; + } + + return node; + }, + + /** + * Check if a descendant node exists at a specific path. + */ + has: function has(root, path) { + var node = root; + + for (var i = 0; i < path.length; i++) { + var p = path[i]; + + if (Text.isText(node) || !node.children[p]) { + return false; + } + + node = node.children[p]; + } + + return true; + }, + + /** + * Check if a value implements the `Node` interface. + */ + isNode: function isNode(value) { + return Text.isText(value) || Element$1.isElement(value) || Editor.isEditor(value); + }, + + /** + * Check if a value is a list of `Node` objects. + */ + isNodeList: function isNodeList(value) { + if (!Array.isArray(value)) { + return false; + } + + var cachedResult = IS_NODE_LIST_CACHE.get(value); + + if (cachedResult !== undefined) { + return cachedResult; + } + + var isNodeList = value.every(function (val) { + return Node$1.isNode(val); + }); + IS_NODE_LIST_CACHE.set(value, isNodeList); + return isNodeList; + }, + + /** + * Get the last node entry in a root node from a path. + */ + last: function last(root, path) { + var p = path.slice(); + var n = Node$1.get(root, p); + + while (n) { + if (Text.isText(n) || n.children.length === 0) { + break; + } else { + var i = n.children.length - 1; + n = n.children[i]; + p.push(i); + } + } + + return [n, p]; + }, + + /** + * Get the node at a specific path, ensuring it's a leaf text node. + */ + leaf: function leaf(root, path) { + var node = Node$1.get(root, path); + + if (!Text.isText(node)) { + throw new Error("Cannot get the leaf node at path [".concat(path, "] because it refers to a non-leaf node: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of the in a branch of the tree, from a specific path. + * + * By default the order is top-down, from lowest to highest node in the tree, + * but you can pass the `reverse: true` option to go bottom-up. + */ + levels: function* levels(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var _iterator5 = _createForOfIteratorHelper$4(Path.levels(path, options)), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var p = _step5.value; + var n = Node$1.get(root, p); + yield [n, p]; + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + }, + + /** + * Check if a node matches a set of props. + */ + matches: function matches(node, props) { + return Element$1.isElement(node) && Element$1.isElementProps(props) && Element$1.matches(node, props) || Text.isText(node) && Text.isTextProps(props) && Text.matches(node, props); + }, + + /** + * Return a generator of all the node entries of a root node. Each entry is + * returned as a `[Node, Path]` tuple, with the path referring to the node's + * position inside the root node. + */ + nodes: function* nodes(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var pass = options.pass, + _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2; + var _options$from = options.from, + from = _options$from === void 0 ? [] : _options$from, + to = options.to; + var visited = new Set(); + var p = []; + var n = root; + + while (true) { + if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) { + break; + } + + if (!visited.has(n)) { + yield [n, p]; + } // If we're allowed to go downward and we haven't descended yet, do. + + + if (!visited.has(n) && !Text.isText(n) && n.children.length !== 0 && (pass == null || pass([n, p]) === false)) { + visited.add(n); + var nextIndex = reverse ? n.children.length - 1 : 0; + + if (Path.isAncestor(p, from)) { + nextIndex = from[p.length]; + } + + p = p.concat(nextIndex); + n = Node$1.get(root, p); + continue; + } // If we're at the root and we can't go down, we're done. + + + if (p.length === 0) { + break; + } // If we're going forward... + + + if (!reverse) { + var newPath = Path.next(p); + + if (Node$1.has(root, newPath)) { + p = newPath; + n = Node$1.get(root, p); + continue; + } + } // If we're going backward... + + + if (reverse && p[p.length - 1] !== 0) { + var _newPath = Path.previous(p); + + p = _newPath; + n = Node$1.get(root, p); + continue; + } // Otherwise we're going upward... + + + p = Path.parent(p); + n = Node$1.get(root, p); + visited.add(n); + } + }, + + /** + * Get the parent of a node at a specific path. + */ + parent: function parent(root, path) { + var parentPath = Path.parent(path); + var p = Node$1.get(root, parentPath); + + if (Text.isText(p)) { + throw new Error("Cannot get the parent of path [".concat(path, "] because it does not exist in the root.")); + } + + return p; + }, + + /** + * Get the concatenated text string of a node's content. + * + * Note that this will not include spaces or line breaks between block nodes. + * It is not a user-facing string, but a string for performing offset-related + * computations for a node. + */ + string: function string(node) { + if (Text.isText(node)) { + return node.text; + } else { + return node.children.map(Node$1.string).join(''); + } + }, + + /** + * Return a generator of all leaf text nodes in a root node. + */ + texts: function* texts(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator6 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _step6$value = _slicedToArray(_step6.value, 2), + node = _step6$value[0], + path = _step6$value[1]; + + if (Text.isText(node)) { + yield [node, path]; + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + } + }; + + function ownKeys$7(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$7(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$7(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$7(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Operation = { + /** + * Check of a value is a `NodeOperation` object. + */ + isNodeOperation: function isNodeOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_node'); + }, + + /** + * Check of a value is an `Operation` object. + */ + isOperation: function isOperation(value) { + if (!isPlainObject.isPlainObject(value)) { + return false; + } + + switch (value.type) { + case 'insert_node': + return Path.isPath(value.path) && Node$1.isNode(value.node); + + case 'insert_text': + return typeof value.offset === 'number' && typeof value.text === 'string' && Path.isPath(value.path); + + case 'merge_node': + return typeof value.position === 'number' && Path.isPath(value.path) && isPlainObject.isPlainObject(value.properties); + + case 'move_node': + return Path.isPath(value.path) && Path.isPath(value.newPath); + + case 'remove_node': + return Path.isPath(value.path) && Node$1.isNode(value.node); + + case 'remove_text': + return typeof value.offset === 'number' && typeof value.text === 'string' && Path.isPath(value.path); + + case 'set_node': + return Path.isPath(value.path) && isPlainObject.isPlainObject(value.properties) && isPlainObject.isPlainObject(value.newProperties); + + case 'set_selection': + return value.properties === null && Range.isRange(value.newProperties) || value.newProperties === null && Range.isRange(value.properties) || isPlainObject.isPlainObject(value.properties) && isPlainObject.isPlainObject(value.newProperties); + + case 'split_node': + return Path.isPath(value.path) && typeof value.position === 'number' && isPlainObject.isPlainObject(value.properties); + + default: + return false; + } + }, + + /** + * Check if a value is a list of `Operation` objects. + */ + isOperationList: function isOperationList(value) { + return Array.isArray(value) && value.every(function (val) { + return Operation.isOperation(val); + }); + }, + + /** + * Check of a value is a `SelectionOperation` object. + */ + isSelectionOperation: function isSelectionOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_selection'); + }, + + /** + * Check of a value is a `TextOperation` object. + */ + isTextOperation: function isTextOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_text'); + }, + + /** + * Invert an operation, returning a new operation that will exactly undo the + * original when applied. + */ + inverse: function inverse(op) { + switch (op.type) { + case 'insert_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'remove_node' + }); + } + + case 'insert_text': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'remove_text' + }); + } + + case 'merge_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'split_node', + path: Path.previous(op.path) + }); + } + + case 'move_node': + { + var newPath = op.newPath, + path = op.path; // PERF: in this case the move operation is a no-op anyways. + + if (Path.equals(newPath, path)) { + return op; + } // If the move happens completely within a single parent the path and + // newPath are stable with respect to each other. + + + if (Path.isSibling(path, newPath)) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + path: newPath, + newPath: path + }); + } // If the move does not happen within a single parent it is possible + // for the move to impact the true path to the location where the node + // was removed from and where it was inserted. We have to adjust for this + // and find the original path. We can accomplish this (only in non-sibling) + // moves by looking at the impact of the move operation on the node + // after the original move path. + + + var inversePath = Path.transform(path, op); + var inverseNewPath = Path.transform(Path.next(path), op); + return _objectSpread$7(_objectSpread$7({}, op), {}, { + path: inversePath, + newPath: inverseNewPath + }); + } + + case 'remove_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'insert_node' + }); + } + + case 'remove_text': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'insert_text' + }); + } + + case 'set_node': + { + var properties = op.properties, + newProperties = op.newProperties; + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: newProperties, + newProperties: properties + }); + } + + case 'set_selection': + { + var _properties = op.properties, + _newProperties = op.newProperties; + + if (_properties == null) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: _newProperties, + newProperties: null + }); + } else if (_newProperties == null) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: null, + newProperties: _properties + }); + } else { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: _newProperties, + newProperties: _properties + }); + } + } + + case 'split_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'merge_node', + path: Path.next(op.path) + }); + } + } + } + }; + + var Path = { + /** + * Get a list of ancestor paths for a given path. + * + * The paths are sorted from deepest to shallowest ancestor. However, if the + * `reverse: true` option is passed, they are reversed. + */ + ancestors: function ancestors(path) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var paths = Path.levels(path, options); + + if (reverse) { + paths = paths.slice(1); + } else { + paths = paths.slice(0, -1); + } + + return paths; + }, + + /** + * Get the common ancestor path of two paths. + */ + common: function common(path, another) { + var common = []; + + for (var i = 0; i < path.length && i < another.length; i++) { + var av = path[i]; + var bv = another[i]; + + if (av !== bv) { + break; + } + + common.push(av); + } + + return common; + }, + + /** + * Compare a path to another, returning an integer indicating whether the path + * was before, at, or after the other. + * + * Note: Two paths of unequal length can still receive a `0` result if one is + * directly above or below the other. If you want exact matching, use + * [[Path.equals]] instead. + */ + compare: function compare(path, another) { + var min = Math.min(path.length, another.length); + + for (var i = 0; i < min; i++) { + if (path[i] < another[i]) return -1; + if (path[i] > another[i]) return 1; + } + + return 0; + }, + + /** + * Check if a path ends after one of the indexes in another. + */ + endsAfter: function endsAfter(path, another) { + var i = path.length - 1; + var as = path.slice(0, i); + var bs = another.slice(0, i); + var av = path[i]; + var bv = another[i]; + return Path.equals(as, bs) && av > bv; + }, + + /** + * Check if a path ends at one of the indexes in another. + */ + endsAt: function endsAt(path, another) { + var i = path.length; + var as = path.slice(0, i); + var bs = another.slice(0, i); + return Path.equals(as, bs); + }, + + /** + * Check if a path ends before one of the indexes in another. + */ + endsBefore: function endsBefore(path, another) { + var i = path.length - 1; + var as = path.slice(0, i); + var bs = another.slice(0, i); + var av = path[i]; + var bv = another[i]; + return Path.equals(as, bs) && av < bv; + }, + + /** + * Check if a path is exactly equal to another. + */ + equals: function equals(path, another) { + return path.length === another.length && path.every(function (n, i) { + return n === another[i]; + }); + }, + + /** + * Check if the path of previous sibling node exists + */ + hasPrevious: function hasPrevious(path) { + return path[path.length - 1] > 0; + }, + + /** + * Check if a path is after another. + */ + isAfter: function isAfter(path, another) { + return Path.compare(path, another) === 1; + }, + + /** + * Check if a path is an ancestor of another. + */ + isAncestor: function isAncestor(path, another) { + return path.length < another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is before another. + */ + isBefore: function isBefore(path, another) { + return Path.compare(path, another) === -1; + }, + + /** + * Check if a path is a child of another. + */ + isChild: function isChild(path, another) { + return path.length === another.length + 1 && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is equal to or an ancestor of another. + */ + isCommon: function isCommon(path, another) { + return path.length <= another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is a descendant of another. + */ + isDescendant: function isDescendant(path, another) { + return path.length > another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is the parent of another. + */ + isParent: function isParent(path, another) { + return path.length + 1 === another.length && Path.compare(path, another) === 0; + }, + + /** + * Check is a value implements the `Path` interface. + */ + isPath: function isPath(value) { + return Array.isArray(value) && (value.length === 0 || typeof value[0] === 'number'); + }, + + /** + * Check if a path is a sibling of another. + */ + isSibling: function isSibling(path, another) { + if (path.length !== another.length) { + return false; + } + + var as = path.slice(0, -1); + var bs = another.slice(0, -1); + var al = path[path.length - 1]; + var bl = another[another.length - 1]; + return al !== bl && Path.equals(as, bs); + }, + + /** + * Get a list of paths at every level down to a path. Note: this is the same + * as `Path.ancestors`, but including the path itself. + * + * The paths are sorted from shallowest to deepest. However, if the `reverse: + * true` option is passed, they are reversed. + */ + levels: function levels(path) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2; + var list = []; + + for (var i = 0; i <= path.length; i++) { + list.push(path.slice(0, i)); + } + + if (reverse) { + list.reverse(); + } + + return list; + }, + + /** + * Given a path, get the path to the next sibling node. + */ + next: function next(path) { + if (path.length === 0) { + throw new Error("Cannot get the next path of a root path [".concat(path, "], because it has no next index.")); + } + + var last = path[path.length - 1]; + return path.slice(0, -1).concat(last + 1); + }, + + /** + * Given a path, return a new path referring to the parent node above it. + */ + parent: function parent(path) { + if (path.length === 0) { + throw new Error("Cannot get the parent path of the root path [".concat(path, "].")); + } + + return path.slice(0, -1); + }, + + /** + * Given a path, get the path to the previous sibling node. + */ + previous: function previous(path) { + if (path.length === 0) { + throw new Error("Cannot get the previous path of a root path [".concat(path, "], because it has no previous index.")); + } + + var last = path[path.length - 1]; + + if (last <= 0) { + throw new Error("Cannot get the previous path of a first child path [".concat(path, "] because it would result in a negative index.")); + } + + return path.slice(0, -1).concat(last - 1); + }, + + /** + * Get a path relative to an ancestor. + */ + relative: function relative(path, ancestor) { + if (!Path.isAncestor(ancestor, path) && !Path.equals(path, ancestor)) { + throw new Error("Cannot get the relative path of [".concat(path, "] inside ancestor [").concat(ancestor, "], because it is not above or equal to the path.")); + } + + return path.slice(ancestor.length); + }, + + /** + * Transform a path by an operation. + */ + transform: function transform(path, operation) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(path, function (p) { + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; // PERF: Exit early if the operation is guaranteed not to have an effect. + + if (!path || (path === null || path === void 0 ? void 0 : path.length) === 0) { + return; + } + + if (p === null) { + return null; + } + + switch (operation.type) { + case 'insert_node': + { + var op = operation.path; + + if (Path.equals(op, p) || Path.endsBefore(op, p) || Path.isAncestor(op, p)) { + p[op.length - 1] += 1; + } + + break; + } + + case 'remove_node': + { + var _op = operation.path; + + if (Path.equals(_op, p) || Path.isAncestor(_op, p)) { + return null; + } else if (Path.endsBefore(_op, p)) { + p[_op.length - 1] -= 1; + } + + break; + } + + case 'merge_node': + { + var _op2 = operation.path, + position = operation.position; + + if (Path.equals(_op2, p) || Path.endsBefore(_op2, p)) { + p[_op2.length - 1] -= 1; + } else if (Path.isAncestor(_op2, p)) { + p[_op2.length - 1] -= 1; + p[_op2.length] += position; + } + + break; + } + + case 'split_node': + { + var _op3 = operation.path, + _position = operation.position; + + if (Path.equals(_op3, p)) { + if (affinity === 'forward') { + p[p.length - 1] += 1; + } else if (affinity === 'backward') ; else { + return null; + } + } else if (Path.endsBefore(_op3, p)) { + p[_op3.length - 1] += 1; + } else if (Path.isAncestor(_op3, p) && path[_op3.length] >= _position) { + p[_op3.length - 1] += 1; + p[_op3.length] -= _position; + } + + break; + } + + case 'move_node': + { + var _op4 = operation.path, + onp = operation.newPath; // If the old and new path are the same, it's a no-op. + + if (Path.equals(_op4, onp)) { + return; + } + + if (Path.isAncestor(_op4, p) || Path.equals(_op4, p)) { + var copy = onp.slice(); + + if (Path.endsBefore(_op4, onp) && _op4.length < onp.length) { + copy[_op4.length - 1] -= 1; + } + + return copy.concat(p.slice(_op4.length)); + } else if (Path.isSibling(_op4, onp) && (Path.isAncestor(onp, p) || Path.equals(onp, p))) { + if (Path.endsBefore(_op4, p)) { + p[_op4.length - 1] -= 1; + } else { + p[_op4.length - 1] += 1; + } + } else if (Path.endsBefore(onp, p) || Path.equals(onp, p) || Path.isAncestor(onp, p)) { + if (Path.endsBefore(_op4, p)) { + p[_op4.length - 1] -= 1; + } + + p[onp.length - 1] += 1; + } else if (Path.endsBefore(_op4, p)) { + if (Path.equals(onp, p)) { + p[onp.length - 1] += 1; + } + + p[_op4.length - 1] -= 1; + } + + break; + } + } + }); + } + }; + + var PathRef = { + /** + * Transform the path ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var path = Path.transform(current, op, { + affinity: affinity + }); + ref.current = path; + + if (path == null) { + ref.unref(); + } + } + }; + + function ownKeys$6(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$6(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$6(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$6(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Point = { + /** + * Compare a point to another, returning an integer indicating whether the + * point was before, at, or after the other. + */ + compare: function compare(point, another) { + var result = Path.compare(point.path, another.path); + + if (result === 0) { + if (point.offset < another.offset) return -1; + if (point.offset > another.offset) return 1; + return 0; + } + + return result; + }, + + /** + * Check if a point is after another. + */ + isAfter: function isAfter(point, another) { + return Point.compare(point, another) === 1; + }, + + /** + * Check if a point is before another. + */ + isBefore: function isBefore(point, another) { + return Point.compare(point, another) === -1; + }, + + /** + * Check if a point is exactly equal to another. + */ + equals: function equals(point, another) { + // PERF: ensure the offsets are equal first since they are cheaper to check. + return point.offset === another.offset && Path.equals(point.path, another.path); + }, + + /** + * Check if a value implements the `Point` interface. + */ + isPoint: function isPoint(value) { + return isPlainObject.isPlainObject(value) && typeof value.offset === 'number' && Path.isPath(value.path); + }, + + /** + * Transform a point by an operation. + */ + transform: function transform(point, op) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(point, function (p) { + if (p === null) { + return null; + } + + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; + var path = p.path, + offset = p.offset; + + switch (op.type) { + case 'insert_node': + case 'move_node': + { + p.path = Path.transform(path, op, options); + break; + } + + case 'insert_text': + { + if (Path.equals(op.path, path) && op.offset <= offset) { + p.offset += op.text.length; + } + + break; + } + + case 'merge_node': + { + if (Path.equals(op.path, path)) { + p.offset += op.position; + } + + p.path = Path.transform(path, op, options); + break; + } + + case 'remove_text': + { + if (Path.equals(op.path, path) && op.offset <= offset) { + p.offset -= Math.min(offset - op.offset, op.text.length); + } + + break; + } + + case 'remove_node': + { + if (Path.equals(op.path, path) || Path.isAncestor(op.path, path)) { + return null; + } + + p.path = Path.transform(path, op, options); + break; + } + + case 'split_node': + { + if (Path.equals(op.path, path)) { + if (op.position === offset && affinity == null) { + return null; + } else if (op.position < offset || op.position === offset && affinity === 'forward') { + p.offset -= op.position; + p.path = Path.transform(path, op, _objectSpread$6(_objectSpread$6({}, options), {}, { + affinity: 'forward' + })); + } + } else { + p.path = Path.transform(path, op, options); + } + + break; + } + } + }); + } + }; + + var PointRef = { + /** + * Transform the point ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var point = Point.transform(current, op, { + affinity: affinity + }); + ref.current = point; + + if (point == null) { + ref.unref(); + } + } + }; + + var _excluded$2 = ["anchor", "focus"]; + + function ownKeys$5(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$5(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$5(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$5(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Range = { + /** + * Get the start and end points of a range, in the order in which they appear + * in the document. + */ + edges: function edges(range) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var anchor = range.anchor, + focus = range.focus; + return Range.isBackward(range) === reverse ? [anchor, focus] : [focus, anchor]; + }, + + /** + * Get the end point of a range. + */ + end: function end(range) { + var _Range$edges = Range.edges(range), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + end = _Range$edges2[1]; + + return end; + }, + + /** + * Check if a range is exactly equal to another. + */ + equals: function equals(range, another) { + return Point.equals(range.anchor, another.anchor) && Point.equals(range.focus, another.focus); + }, + + /** + * Check if a range includes a path, a point or part of another range. + */ + includes: function includes(range, target) { + if (Range.isRange(target)) { + if (Range.includes(range, target.anchor) || Range.includes(range, target.focus)) { + return true; + } + + var _Range$edges3 = Range.edges(range), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + rs = _Range$edges4[0], + re = _Range$edges4[1]; + + var _Range$edges5 = Range.edges(target), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + ts = _Range$edges6[0], + te = _Range$edges6[1]; + + return Point.isBefore(rs, ts) && Point.isAfter(re, te); + } + + var _Range$edges7 = Range.edges(range), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; + + var isAfterStart = false; + var isBeforeEnd = false; + + if (Point.isPoint(target)) { + isAfterStart = Point.compare(target, start) >= 0; + isBeforeEnd = Point.compare(target, end) <= 0; + } else { + isAfterStart = Path.compare(target, start.path) >= 0; + isBeforeEnd = Path.compare(target, end.path) <= 0; + } + + return isAfterStart && isBeforeEnd; + }, + + /** + * Get the intersection of a range with another. + */ + intersection: function intersection(range, another) { + range.anchor; + range.focus; + var rest = _objectWithoutProperties(range, _excluded$2); + + var _Range$edges9 = Range.edges(range), + _Range$edges10 = _slicedToArray(_Range$edges9, 2), + s1 = _Range$edges10[0], + e1 = _Range$edges10[1]; + + var _Range$edges11 = Range.edges(another), + _Range$edges12 = _slicedToArray(_Range$edges11, 2), + s2 = _Range$edges12[0], + e2 = _Range$edges12[1]; + + var start = Point.isBefore(s1, s2) ? s2 : s1; + var end = Point.isBefore(e1, e2) ? e1 : e2; + + if (Point.isBefore(end, start)) { + return null; + } else { + return _objectSpread$5({ + anchor: start, + focus: end + }, rest); + } + }, + + /** + * Check if a range is backward, meaning that its anchor point appears in the + * document _after_ its focus point. + */ + isBackward: function isBackward(range) { + var anchor = range.anchor, + focus = range.focus; + return Point.isAfter(anchor, focus); + }, + + /** + * Check if a range is collapsed, meaning that both its anchor and focus + * points refer to the exact same position in the document. + */ + isCollapsed: function isCollapsed(range) { + var anchor = range.anchor, + focus = range.focus; + return Point.equals(anchor, focus); + }, + + /** + * Check if a range is expanded. + * + * This is the opposite of [[Range.isCollapsed]] and is provided for legibility. + */ + isExpanded: function isExpanded(range) { + return !Range.isCollapsed(range); + }, + + /** + * Check if a range is forward. + * + * This is the opposite of [[Range.isBackward]] and is provided for legibility. + */ + isForward: function isForward(range) { + return !Range.isBackward(range); + }, + + /** + * Check if a value implements the [[Range]] interface. + */ + isRange: function isRange(value) { + return isPlainObject.isPlainObject(value) && Point.isPoint(value.anchor) && Point.isPoint(value.focus); + }, + + /** + * Iterate through all of the point entries in a range. + */ + points: function* points(range) { + yield [range.anchor, 'anchor']; + yield [range.focus, 'focus']; + }, + + /** + * Get the start point of a range. + */ + start: function start(range) { + var _Range$edges13 = Range.edges(range), + _Range$edges14 = _slicedToArray(_Range$edges13, 1), + start = _Range$edges14[0]; + + return start; + }, + + /** + * Transform a range by an operation. + */ + transform: function transform(range, op) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(range, function (r) { + if (r === null) { + return null; + } + + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'inward' : _options$affinity; + var affinityAnchor; + var affinityFocus; + + if (affinity === 'inward') { + // If the range is collapsed, make sure to use the same affinity to + // avoid the two points passing each other and expanding in the opposite + // direction + var isCollapsed = Range.isCollapsed(r); + + if (Range.isForward(r)) { + affinityAnchor = 'forward'; + affinityFocus = isCollapsed ? affinityAnchor : 'backward'; + } else { + affinityAnchor = 'backward'; + affinityFocus = isCollapsed ? affinityAnchor : 'forward'; + } + } else if (affinity === 'outward') { + if (Range.isForward(r)) { + affinityAnchor = 'backward'; + affinityFocus = 'forward'; + } else { + affinityAnchor = 'forward'; + affinityFocus = 'backward'; + } + } else { + affinityAnchor = affinity; + affinityFocus = affinity; + } + + var anchor = Point.transform(r.anchor, op, { + affinity: affinityAnchor + }); + var focus = Point.transform(r.focus, op, { + affinity: affinityFocus + }); + + if (!anchor || !focus) { + return null; + } + + r.anchor = anchor; + r.focus = focus; + }); + } + }; + + var RangeRef = { + /** + * Transform the range ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var path = Range.transform(current, op, { + affinity: affinity + }); + ref.current = path; + + if (path == null) { + ref.unref(); + } + } + }; + + /* + Custom deep equal comparison for Slate nodes. + + We don't need general purpose deep equality; + Slate only supports plain values, Arrays, and nested objects. + Complex values nested inside Arrays are not supported. + + Slate objects are designed to be serialised, so + missing keys are deliberately normalised to undefined. + */ + + var isDeepEqual = function isDeepEqual(node, another) { + for (var key in node) { + var a = node[key]; + var b = another[key]; + + if (isPlainObject.isPlainObject(a) && isPlainObject.isPlainObject(b)) { + if (!isDeepEqual(a, b)) return false; + } else if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + } else if (a !== b) { + return false; + } + } + /* + Deep object equality is only necessary in one direction; in the reverse direction + we are only looking for keys that are missing. + As above, undefined keys are normalised to missing. + */ + + + for (var _key in another) { + if (node[_key] === undefined && another[_key] !== undefined) { + return false; + } + } + + return true; + }; + + var _excluded$1 = ["text"], + _excluded2$1 = ["anchor", "focus"]; + + function _createForOfIteratorHelper$3(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$3(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$3(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$3(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$3(o, minLen); } + + function _arrayLikeToArray$3(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + function ownKeys$4(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$4(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$4(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$4(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Text = { + /** + * Check if two text nodes are equal. + * + * When loose is set, the text is not compared. This is + * used to check whether sibling text nodes can be merged. + */ + equals: function equals(text, another) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$loose = options.loose, + loose = _options$loose === void 0 ? false : _options$loose; + + function omitText(obj) { + obj.text; + var rest = _objectWithoutProperties(obj, _excluded$1); + + return rest; + } + + return isDeepEqual(loose ? omitText(text) : text, loose ? omitText(another) : another); + }, + + /** + * Check if a value implements the `Text` interface. + */ + isText: function isText(value) { + return isPlainObject.isPlainObject(value) && typeof value.text === 'string'; + }, + + /** + * Check if a value is a list of `Text` objects. + */ + isTextList: function isTextList(value) { + return Array.isArray(value) && value.every(function (val) { + return Text.isText(val); + }); + }, + + /** + * Check if some props are a partial of Text. + */ + isTextProps: function isTextProps(props) { + return props.text !== undefined; + }, + + /** + * Check if an text matches set of properties. + * + * Note: this is for matching custom properties, and it does not ensure that + * the `text` property are two nodes equal. + */ + matches: function matches(text, props) { + for (var key in props) { + if (key === 'text') { + continue; + } + + if (!text.hasOwnProperty(key) || text[key] !== props[key]) { + return false; + } + } + + return true; + }, + + /** + * Get the leaves for a text node given decorations. + */ + decorations: function decorations(node, _decorations) { + var leaves = [_objectSpread$4({}, node)]; + + var _iterator = _createForOfIteratorHelper$3(_decorations), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var dec = _step.value; + + var anchor = dec.anchor, + focus = dec.focus, + rest = _objectWithoutProperties(dec, _excluded2$1); + + var _Range$edges = Range.edges(dec), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + var next = []; + var o = 0; + + var _iterator2 = _createForOfIteratorHelper$3(leaves), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var leaf = _step2.value; + var length = leaf.text.length; + var offset = o; + o += length; // If the range encompases the entire leaf, add the range. + + if (start.offset <= offset && end.offset >= o) { + Object.assign(leaf, rest); + next.push(leaf); + continue; + } // If the range expanded and match the leaf, or starts after, or ends before it, continue. + + + if (start.offset !== end.offset && (start.offset === o || end.offset === offset) || start.offset > o || end.offset < offset || end.offset === offset && offset !== 0) { + next.push(leaf); + continue; + } // Otherwise we need to split the leaf, at the start, end, or both, + // and add the range to the middle intersecting section. Do the end + // split first since we don't need to update the offset that way. + + + var middle = leaf; + var before = void 0; + var after = void 0; + + if (end.offset < o) { + var off = end.offset - offset; + after = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(off) + }); + middle = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(0, off) + }); + } + + if (start.offset > offset) { + var _off = start.offset - offset; + + before = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(0, _off) + }); + middle = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(_off) + }); + } + + Object.assign(middle, rest); + + if (before) { + next.push(before); + } + + next.push(middle); + + if (after) { + next.push(after); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + leaves = next; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return leaves; + } + }; + + function ownKeys$3(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$3(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$3(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$3(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$2(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$2(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$2(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$2(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$2(o, minLen); } + + function _arrayLikeToArray$2(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + var applyToDraft = function applyToDraft(editor, selection, op) { + switch (op.type) { + case 'insert_node': + { + var path = op.path, + node = op.node; + var parent = Node$1.parent(editor, path); + var index = path[path.length - 1]; + + if (index > parent.children.length) { + throw new Error("Cannot apply an \"insert_node\" operation at path [".concat(path, "] because the destination is past the end of the node.")); + } + + parent.children.splice(index, 0, node); + + if (selection) { + var _iterator = _createForOfIteratorHelper$2(Range.points(selection)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _step$value = _slicedToArray(_step.value, 2), + point = _step$value[0], + key = _step$value[1]; + + selection[key] = Point.transform(point, op); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + break; + } + + case 'insert_text': + { + var _path = op.path, + offset = op.offset, + text = op.text; + if (text.length === 0) break; + + var _node = Node$1.leaf(editor, _path); + + var before = _node.text.slice(0, offset); + + var after = _node.text.slice(offset); + + _node.text = before + text + after; + + if (selection) { + var _iterator2 = _createForOfIteratorHelper$2(Range.points(selection)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + _point = _step2$value[0], + _key = _step2$value[1]; + + selection[_key] = Point.transform(_point, op); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + } + + break; + } + + case 'merge_node': + { + var _path2 = op.path; + + var _node2 = Node$1.get(editor, _path2); + + var prevPath = Path.previous(_path2); + var prev = Node$1.get(editor, prevPath); + + var _parent = Node$1.parent(editor, _path2); + + var _index = _path2[_path2.length - 1]; + + if (Text.isText(_node2) && Text.isText(prev)) { + prev.text += _node2.text; + } else if (!Text.isText(_node2) && !Text.isText(prev)) { + var _prev$children; + + (_prev$children = prev.children).push.apply(_prev$children, _toConsumableArray(_node2.children)); + } else { + throw new Error("Cannot apply a \"merge_node\" operation at path [".concat(_path2, "] to nodes of different interfaces: ").concat(_node2, " ").concat(prev)); + } + + _parent.children.splice(_index, 1); + + if (selection) { + var _iterator3 = _createForOfIteratorHelper$2(Range.points(selection)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + _point2 = _step3$value[0], + _key2 = _step3$value[1]; + + selection[_key2] = Point.transform(_point2, op); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + } + + break; + } + + case 'move_node': + { + var _path3 = op.path, + newPath = op.newPath; + + if (Path.isAncestor(_path3, newPath)) { + throw new Error("Cannot move a path [".concat(_path3, "] to new path [").concat(newPath, "] because the destination is inside itself.")); + } + + var _node3 = Node$1.get(editor, _path3); + + var _parent2 = Node$1.parent(editor, _path3); + + var _index2 = _path3[_path3.length - 1]; // This is tricky, but since the `path` and `newPath` both refer to + // the same snapshot in time, there's a mismatch. After either + // removing the original position, the second step's path can be out + // of date. So instead of using the `op.newPath` directly, we + // transform `op.path` to ascertain what the `newPath` would be after + // the operation was applied. + + _parent2.children.splice(_index2, 1); + + var truePath = Path.transform(_path3, op); + var newParent = Node$1.get(editor, Path.parent(truePath)); + var newIndex = truePath[truePath.length - 1]; + newParent.children.splice(newIndex, 0, _node3); + + if (selection) { + var _iterator4 = _createForOfIteratorHelper$2(Range.points(selection)), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + _point3 = _step4$value[0], + _key3 = _step4$value[1]; + + selection[_key3] = Point.transform(_point3, op); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + } + + break; + } + + case 'remove_node': + { + var _path4 = op.path; + var _index3 = _path4[_path4.length - 1]; + + var _parent3 = Node$1.parent(editor, _path4); + + _parent3.children.splice(_index3, 1); // Transform all of the points in the value, but if the point was in the + // node that was removed we need to update the range or remove it. + + + if (selection) { + var _iterator5 = _createForOfIteratorHelper$2(Range.points(selection)), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _step5$value = _slicedToArray(_step5.value, 2), + _point4 = _step5$value[0], + _key4 = _step5$value[1]; + + var result = Point.transform(_point4, op); + + if (selection != null && result != null) { + selection[_key4] = result; + } else { + var _prev = void 0; + + var next = void 0; + + var _iterator6 = _createForOfIteratorHelper$2(Node$1.texts(editor)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _step6$value = _slicedToArray(_step6.value, 2), + n = _step6$value[0], + p = _step6$value[1]; + + if (Path.compare(p, _path4) === -1) { + _prev = [n, p]; + } else { + next = [n, p]; + break; + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var preferNext = false; + + if (_prev && next) { + if (Path.equals(next[1], _path4)) { + preferNext = !Path.hasPrevious(next[1]); + } else { + preferNext = Path.common(_prev[1], _path4).length < Path.common(next[1], _path4).length; + } + } + + if (_prev && !preferNext) { + _point4.path = _prev[1]; + _point4.offset = _prev[0].text.length; + } else if (next) { + _point4.path = next[1]; + _point4.offset = 0; + } else { + selection = null; + } + } + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + } + + break; + } + + case 'remove_text': + { + var _path5 = op.path, + _offset = op.offset, + _text = op.text; + if (_text.length === 0) break; + + var _node4 = Node$1.leaf(editor, _path5); + + var _before = _node4.text.slice(0, _offset); + + var _after = _node4.text.slice(_offset + _text.length); + + _node4.text = _before + _after; + + if (selection) { + var _iterator7 = _createForOfIteratorHelper$2(Range.points(selection)), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _step7$value = _slicedToArray(_step7.value, 2), + _point5 = _step7$value[0], + _key5 = _step7$value[1]; + + selection[_key5] = Point.transform(_point5, op); + } + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + } + + break; + } + + case 'set_node': + { + var _path6 = op.path, + properties = op.properties, + newProperties = op.newProperties; + + if (_path6.length === 0) { + throw new Error("Cannot set properties on the root node!"); + } + + var _node5 = Node$1.get(editor, _path6); + + for (var _key6 in newProperties) { + if (_key6 === 'children' || _key6 === 'text') { + throw new Error("Cannot set the \"".concat(_key6, "\" property of nodes!")); + } + + var value = newProperties[_key6]; + + if (value == null) { + delete _node5[_key6]; + } else { + _node5[_key6] = value; + } + } // properties that were previously defined, but are now missing, must be deleted + + + for (var _key7 in properties) { + if (!newProperties.hasOwnProperty(_key7)) { + delete _node5[_key7]; + } + } + + break; + } + + case 'set_selection': + { + var _newProperties = op.newProperties; + + if (_newProperties == null) { + selection = _newProperties; + } else { + if (selection == null) { + if (!Range.isRange(_newProperties)) { + throw new Error("Cannot apply an incomplete \"set_selection\" operation properties ".concat(JSON.stringify(_newProperties), " when there is no current selection.")); + } + + selection = _objectSpread$3({}, _newProperties); + } + + for (var _key8 in _newProperties) { + var _value = _newProperties[_key8]; + + if (_value == null) { + if (_key8 === 'anchor' || _key8 === 'focus') { + throw new Error("Cannot remove the \"".concat(_key8, "\" selection property")); + } + + delete selection[_key8]; + } else { + selection[_key8] = _value; + } + } + } + + break; + } + + case 'split_node': + { + var _path7 = op.path, + position = op.position, + _properties = op.properties; + + if (_path7.length === 0) { + throw new Error("Cannot apply a \"split_node\" operation at path [".concat(_path7, "] because the root node cannot be split.")); + } + + var _node6 = Node$1.get(editor, _path7); + + var _parent4 = Node$1.parent(editor, _path7); + + var _index4 = _path7[_path7.length - 1]; + var newNode; + + if (Text.isText(_node6)) { + var _before2 = _node6.text.slice(0, position); + + var _after2 = _node6.text.slice(position); + + _node6.text = _before2; + newNode = _objectSpread$3(_objectSpread$3({}, _properties), {}, { + text: _after2 + }); + } else { + var _before3 = _node6.children.slice(0, position); + + var _after3 = _node6.children.slice(position); + + _node6.children = _before3; + newNode = _objectSpread$3(_objectSpread$3({}, _properties), {}, { + children: _after3 + }); + } + + _parent4.children.splice(_index4 + 1, 0, newNode); + + if (selection) { + var _iterator8 = _createForOfIteratorHelper$2(Range.points(selection)), + _step8; + + try { + for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { + var _step8$value = _slicedToArray(_step8.value, 2), + _point6 = _step8$value[0], + _key9 = _step8$value[1]; + + selection[_key9] = Point.transform(_point6, op); + } + } catch (err) { + _iterator8.e(err); + } finally { + _iterator8.f(); + } + } + + break; + } + } + + return selection; + }; + + var GeneralTransforms = { + /** + * Transform the editor by an operation. + */ + transform: function transform(editor, op) { + editor.children = immer.createDraft(editor.children); + var selection = editor.selection && immer.createDraft(editor.selection); + + try { + selection = applyToDraft(editor, selection, op); + } finally { + editor.children = immer.finishDraft(editor.children); + + if (selection) { + editor.selection = immer.isDraft(selection) ? immer.finishDraft(selection) : selection; + } else { + editor.selection = null; + } + } + } + }; + + var _excluded = ["text"], + _excluded2 = ["children"]; + + function ownKeys$2(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$2(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$2(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$1(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$1(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$1(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$1(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$1(o, minLen); } + + function _arrayLikeToArray$1(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var NodeTransforms = { + /** + * Insert nodes at a specific location in the Editor. + */ + insertNodes: function insertNodes(editor, nodes) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging = options.hanging, + hanging = _options$hanging === void 0 ? false : _options$hanging, + _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids, + _options$mode = options.mode, + mode = _options$mode === void 0 ? 'lowest' : _options$mode; + var at = options.at, + match = options.match, + select = options.select; + + if (Node$1.isNode(nodes)) { + nodes = [nodes]; + } + + if (nodes.length === 0) { + return; + } + + var _nodes = nodes, + _nodes2 = _slicedToArray(_nodes, 1), + node = _nodes2[0]; // By default, use the selection as the target location. But if there is + // no selection, insert at the end of the document since that is such a + // common use case when inserting from a non-selected state. + + + if (!at) { + if (editor.selection) { + at = editor.selection; + } else if (editor.children.length > 0) { + at = Editor.end(editor, []); + } else { + at = [0]; + } + + select = true; + } + + if (select == null) { + select = false; + } + + if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + end = _Range$edges2[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + } + } + + if (Point.isPoint(at)) { + if (match == null) { + if (Text.isText(node)) { + match = function match(n) { + return Text.isText(n); + }; + } else if (editor.isInline(node)) { + match = function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + var _Editor$nodes = Editor.nodes(editor, { + at: at.path, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + entry = _Editor$nodes2[0]; + + if (entry) { + var _entry = _slicedToArray(entry, 2), + _matchPath = _entry[1]; + + var pathRef = Editor.pathRef(editor, _matchPath); + var isAtEnd = Editor.isEnd(editor, at, _matchPath); + Transforms.splitNodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var path = pathRef.unref(); + at = isAtEnd ? Path.next(path) : path; + } else { + return; + } + } + + var parentPath = Path.parent(at); + var index = at[at.length - 1]; + + if (!voids && Editor["void"](editor, { + at: parentPath + })) { + return; + } + + var _iterator = _createForOfIteratorHelper$1(nodes), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _node = _step.value; + + var _path = parentPath.concat(index); + + index++; + editor.apply({ + type: 'insert_node', + path: _path, + node: _node + }); + at = Path.next(at); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + at = Path.previous(at); + + if (select) { + var point = Editor.end(editor, at); + + if (point) { + Transforms.select(editor, point); + } + } + }); + }, + + /** + * Lift nodes at a specific location upwards in the document tree, splitting + * their parent in two if necessary. + */ + liftNodes: function liftNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + _options$mode2 = options.mode, + mode = _options$mode2 === void 0 ? 'lowest' : _options$mode2, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var match = options.match; + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!at) { + return; + } + + var matches = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(matches, function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + p = _ref2[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i = 0, _pathRefs = pathRefs; _i < _pathRefs.length; _i++) { + var pathRef = _pathRefs[_i]; + var path = pathRef.unref(); + + if (path.length < 2) { + throw new Error("Cannot lift node at a path [".concat(path, "] because it has a depth of less than `2`.")); + } + + var parentNodeEntry = Editor.node(editor, Path.parent(path)); + + var _parentNodeEntry = _slicedToArray(parentNodeEntry, 2), + parent = _parentNodeEntry[0], + parentPath = _parentNodeEntry[1]; + + var index = path[path.length - 1]; + var length = parent.children.length; + + if (length === 1) { + var toPath = Path.next(parentPath); + Transforms.moveNodes(editor, { + at: path, + to: toPath, + voids: voids + }); + Transforms.removeNodes(editor, { + at: parentPath, + voids: voids + }); + } else if (index === 0) { + Transforms.moveNodes(editor, { + at: path, + to: parentPath, + voids: voids + }); + } else if (index === length - 1) { + var _toPath = Path.next(parentPath); + + Transforms.moveNodes(editor, { + at: path, + to: _toPath, + voids: voids + }); + } else { + var splitPath = Path.next(path); + + var _toPath2 = Path.next(parentPath); + + Transforms.splitNodes(editor, { + at: splitPath, + voids: voids + }); + Transforms.moveNodes(editor, { + at: path, + to: _toPath2, + voids: voids + }); + } + } + }); + }, + + /** + * Merge a node at a location with the previous node of the same depth, + * removing any empty containing nodes after the merge if necessary. + */ + mergeNodes: function mergeNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var match = options.match, + _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2; + var _options$hanging2 = options.hanging, + hanging = _options$hanging2 === void 0 ? false : _options$hanging2, + _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3, + _options$mode3 = options.mode, + mode = _options$mode3 === void 0 ? 'lowest' : _options$mode3; + + if (!at) { + return; + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent = Editor.parent(editor, at), + _Editor$parent2 = _slicedToArray(_Editor$parent, 1), + parent = _Editor$parent2[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isRange(at)) { + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges3 = Range.edges(at), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + end = _Range$edges4[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + } + + var _Editor$nodes3 = Editor.nodes(editor, { + at: at, + match: match, + voids: voids, + mode: mode + }), + _Editor$nodes4 = _slicedToArray(_Editor$nodes3, 1), + current = _Editor$nodes4[0]; + + var prev = Editor.previous(editor, { + at: at, + match: match, + voids: voids, + mode: mode + }); + + if (!current || !prev) { + return; + } + + var _current = _slicedToArray(current, 2), + node = _current[0], + path = _current[1]; + + var _prev = _slicedToArray(prev, 2), + prevNode = _prev[0], + prevPath = _prev[1]; + + if (path.length === 0 || prevPath.length === 0) { + return; + } + + var newPath = Path.next(prevPath); + var commonPath = Path.common(path, prevPath); + var isPreviousSibling = Path.isSibling(path, prevPath); + var levels = Array.from(Editor.levels(editor, { + at: path + }), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 1), + n = _ref4[0]; + + return n; + }).slice(commonPath.length).slice(0, -1); // Determine if the merge will leave an ancestor of the path empty as a + // result, in which case we'll want to remove it after merging. + + var emptyAncestor = Editor.above(editor, { + at: path, + mode: 'highest', + match: function match(n) { + return levels.includes(n) && hasSingleChildNest(editor, n); + } + }); + var emptyRef = emptyAncestor && Editor.pathRef(editor, emptyAncestor[1]); + var properties; + var position; // Ensure that the nodes are equivalent, and figure out what the position + // and extra properties of the merge will be. + + if (Text.isText(node) && Text.isText(prevNode)) { + node.text; + var rest = _objectWithoutProperties(node, _excluded); + + position = prevNode.text.length; + properties = rest; + } else if (Element$1.isElement(node) && Element$1.isElement(prevNode)) { + node.children; + var _rest = _objectWithoutProperties(node, _excluded2); + + position = prevNode.children.length; + properties = _rest; + } else { + throw new Error("Cannot merge the node at path [".concat(path, "] with the previous sibling because it is not the same kind: ").concat(JSON.stringify(node), " ").concat(JSON.stringify(prevNode))); + } // If the node isn't already the next sibling of the previous node, move + // it so that it is before merging. + + + if (!isPreviousSibling) { + Transforms.moveNodes(editor, { + at: path, + to: newPath, + voids: voids + }); + } // If there was going to be an empty ancestor of the node that was merged, + // we remove it from the tree. + + + if (emptyRef) { + Transforms.removeNodes(editor, { + at: emptyRef.current, + voids: voids + }); + } // If the target node that we're merging with is empty, remove it instead + // of merging the two. This is a common rich text editor behavior to + // prevent losing formatting when deleting entire nodes when you have a + // hanging selection. + // if prevNode is first child in parent,don't remove it. + + + if (Element$1.isElement(prevNode) && Editor.isEmpty(editor, prevNode) || Text.isText(prevNode) && prevNode.text === '' && prevPath[prevPath.length - 1] !== 0) { + Transforms.removeNodes(editor, { + at: prevPath, + voids: voids + }); + } else { + editor.apply({ + type: 'merge_node', + path: newPath, + position: position, + properties: properties + }); + } + + if (emptyRef) { + emptyRef.unref(); + } + }); + }, + + /** + * Move the nodes at a location to a new location. + */ + moveNodes: function moveNodes(editor, options) { + Editor.withoutNormalizing(editor, function () { + var to = options.to, + _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3, + _options$mode4 = options.mode, + mode = _options$mode4 === void 0 ? 'lowest' : _options$mode4, + _options$voids4 = options.voids, + voids = _options$voids4 === void 0 ? false : _options$voids4; + var match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + var toRef = Editor.pathRef(editor, to); + var targets = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(targets, function (_ref5) { + var _ref6 = _slicedToArray(_ref5, 2), + p = _ref6[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i2 = 0, _pathRefs2 = pathRefs; _i2 < _pathRefs2.length; _i2++) { + var pathRef = _pathRefs2[_i2]; + var path = pathRef.unref(); + var newPath = toRef.current; + + if (path.length !== 0) { + editor.apply({ + type: 'move_node', + path: path, + newPath: newPath + }); + } + + if (toRef.current && Path.isSibling(newPath, path) && Path.isAfter(newPath, path)) { + // When performing a sibling move to a later index, the path at the destination is shifted + // to before the insertion point instead of after. To ensure our group of nodes are inserted + // in the correct order we increment toRef to account for that + toRef.current = Path.next(toRef.current); + } + } + + toRef.unref(); + }); + }, + + /** + * Remove the nodes at a specific location in the document. + */ + removeNodes: function removeNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging3 = options.hanging, + hanging = _options$hanging3 === void 0 ? false : _options$hanging3, + _options$voids5 = options.voids, + voids = _options$voids5 === void 0 ? false : _options$voids5, + _options$mode5 = options.mode, + mode = _options$mode5 === void 0 ? 'lowest' : _options$mode5; + var _options$at4 = options.at, + at = _options$at4 === void 0 ? editor.selection : _options$at4, + match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + var depths = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(depths, function (_ref7) { + var _ref8 = _slicedToArray(_ref7, 2), + p = _ref8[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i3 = 0, _pathRefs3 = pathRefs; _i3 < _pathRefs3.length; _i3++) { + var pathRef = _pathRefs3[_i3]; + var path = pathRef.unref(); + + if (path) { + var _Editor$node = Editor.node(editor, path), + _Editor$node2 = _slicedToArray(_Editor$node, 1), + node = _Editor$node2[0]; + + editor.apply({ + type: 'remove_node', + path: path, + node: node + }); + } + } + }); + }, + + /** + * Set new properties on the nodes at a location. + */ + setNodes: function setNodes(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var match = options.match, + _options$at5 = options.at, + at = _options$at5 === void 0 ? editor.selection : _options$at5; + var _options$hanging4 = options.hanging, + hanging = _options$hanging4 === void 0 ? false : _options$hanging4, + _options$mode6 = options.mode, + mode = _options$mode6 === void 0 ? 'lowest' : _options$mode6, + _options$split = options.split, + split = _options$split === void 0 ? false : _options$split, + _options$voids6 = options.voids, + voids = _options$voids6 === void 0 ? false : _options$voids6; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + if (split && Range.isRange(at)) { + if (Range.isCollapsed(at) && Editor.leaf(editor, at.anchor)[0].text.length > 0) { + // If the range is collapsed in a non-empty node and 'split' is true, there's nothing to + // set that won't get normalized away + return; + } + + var rangeRef = Editor.rangeRef(editor, at, { + affinity: 'inward' + }); + + var _Range$edges5 = Range.edges(at), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + start = _Range$edges6[0], + end = _Range$edges6[1]; + + var splitMode = mode === 'lowest' ? 'lowest' : 'highest'; + var endAtEndOfNode = Editor.isEnd(editor, end, end.path); + Transforms.splitNodes(editor, { + at: end, + match: match, + mode: splitMode, + voids: voids, + always: !endAtEndOfNode + }); + var startAtStartOfNode = Editor.isStart(editor, start, start.path); + Transforms.splitNodes(editor, { + at: start, + match: match, + mode: splitMode, + voids: voids, + always: !startAtStartOfNode + }); + at = rangeRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + + var _iterator2 = _createForOfIteratorHelper$1(Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + })), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + node = _step2$value[0], + path = _step2$value[1]; + + var properties = {}; + var newProperties = {}; // You can't set properties on the editor node. + + if (path.length === 0) { + continue; + } + + var hasChanges = false; + + for (var k in props) { + if (k === 'children' || k === 'text') { + continue; + } + + if (props[k] !== node[k]) { + hasChanges = true; // Omit new properties from the old properties list + + if (node.hasOwnProperty(k)) properties[k] = node[k]; // Omit properties that have been removed from the new properties list + + if (props[k] != null) newProperties[k] = props[k]; + } + } + + if (hasChanges) { + editor.apply({ + type: 'set_node', + path: path, + properties: properties, + newProperties: newProperties + }); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + }); + }, + + /** + * Split the nodes at a specific location. + */ + splitNodes: function splitNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode7 = options.mode, + mode = _options$mode7 === void 0 ? 'lowest' : _options$mode7, + _options$voids7 = options.voids, + voids = _options$voids7 === void 0 ? false : _options$voids7; + var match = options.match, + _options$at6 = options.at, + at = _options$at6 === void 0 ? editor.selection : _options$at6, + _options$height = options.height, + height = _options$height === void 0 ? 0 : _options$height, + _options$always = options.always, + always = _options$always === void 0 ? false : _options$always; + + if (match == null) { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + + if (Range.isRange(at)) { + at = deleteRange(editor, at); + } // If the target is a path, the default height-skipping and position + // counters need to account for us potentially splitting at a non-leaf. + + + if (Path.isPath(at)) { + var path = at; + var point = Editor.point(editor, path); + + var _Editor$parent3 = Editor.parent(editor, path), + _Editor$parent4 = _slicedToArray(_Editor$parent3, 1), + parent = _Editor$parent4[0]; + + match = function match(n) { + return n === parent; + }; + + height = point.path.length - path.length + 1; + at = point; + always = true; + } + + if (!at) { + return; + } + + var beforeRef = Editor.pointRef(editor, at, { + affinity: 'backward' + }); + + var _Editor$nodes5 = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes6 = _slicedToArray(_Editor$nodes5, 1), + highest = _Editor$nodes6[0]; + + if (!highest) { + return; + } + + var voidMatch = Editor["void"](editor, { + at: at, + mode: 'highest' + }); + var nudge = 0; + + if (!voids && voidMatch) { + var _voidMatch = _slicedToArray(voidMatch, 2), + voidNode = _voidMatch[0], + voidPath = _voidMatch[1]; + + if (Element$1.isElement(voidNode) && editor.isInline(voidNode)) { + var after = Editor.after(editor, voidPath); + + if (!after) { + var text = { + text: '' + }; + var afterPath = Path.next(voidPath); + Transforms.insertNodes(editor, text, { + at: afterPath, + voids: voids + }); + after = Editor.point(editor, afterPath); + } + + at = after; + always = true; + } + + var siblingHeight = at.path.length - voidPath.length; + height = siblingHeight + 1; + always = true; + } + + var afterRef = Editor.pointRef(editor, at); + var depth = at.path.length - height; + + var _highest = _slicedToArray(highest, 2), + highestPath = _highest[1]; + + var lowestPath = at.path.slice(0, depth); + var position = height === 0 ? at.offset : at.path[depth] + nudge; + + var _iterator3 = _createForOfIteratorHelper$1(Editor.levels(editor, { + at: lowestPath, + reverse: true, + voids: voids + })), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + node = _step3$value[0], + _path2 = _step3$value[1]; + + var split = false; + + if (_path2.length < highestPath.length || _path2.length === 0 || !voids && Editor.isVoid(editor, node)) { + break; + } + + var _point2 = beforeRef.current; + var isEnd = Editor.isEnd(editor, _point2, _path2); + + if (always || !beforeRef || !Editor.isEdge(editor, _point2, _path2)) { + split = true; + var properties = Node$1.extractProps(node); + editor.apply({ + type: 'split_node', + path: _path2, + position: position, + properties: properties + }); + } + + position = _path2[_path2.length - 1] + (split || isEnd ? 1 : 0); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + if (options.at == null) { + var _point = afterRef.current || Editor.end(editor, []); + + Transforms.select(editor, _point); + } + + beforeRef.unref(); + afterRef.unref(); + }); + }, + + /** + * Unset properties on the nodes at a location. + */ + unsetNodes: function unsetNodes(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + if (!Array.isArray(props)) { + props = [props]; + } + + var obj = {}; + + var _iterator4 = _createForOfIteratorHelper$1(props), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var key = _step4.value; + obj[key] = null; + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + Transforms.setNodes(editor, obj, options); + }, + + /** + * Unwrap the nodes at a location from a parent node, splitting the parent if + * necessary to ensure that only the content in the range is unwrapped. + */ + unwrapNodes: function unwrapNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode8 = options.mode, + mode = _options$mode8 === void 0 ? 'lowest' : _options$mode8, + _options$split2 = options.split, + split = _options$split2 === void 0 ? false : _options$split2, + _options$voids8 = options.voids, + voids = _options$voids8 === void 0 ? false : _options$voids8; + var _options$at7 = options.at, + at = _options$at7 === void 0 ? editor.selection : _options$at7, + match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (Path.isPath(at)) { + at = Editor.range(editor, at); + } + + var rangeRef = Range.isRange(at) ? Editor.rangeRef(editor, at) : null; + var matches = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(matches, function (_ref9) { + var _ref10 = _slicedToArray(_ref9, 2), + p = _ref10[1]; + + return Editor.pathRef(editor, p); + } // unwrapNode will call liftNode which does not support splitting the node when nested. + // If we do not reverse the order and call it from top to the bottom, it will remove all blocks + // that wrap target node. So we reverse the order. + ).reverse(); + + var _iterator5 = _createForOfIteratorHelper$1(pathRefs), + _step5; + + try { + var _loop = function _loop() { + var pathRef = _step5.value; + var path = pathRef.unref(); + + var _Editor$node3 = Editor.node(editor, path), + _Editor$node4 = _slicedToArray(_Editor$node3, 1), + node = _Editor$node4[0]; + + var range = Editor.range(editor, path); + + if (split && rangeRef) { + range = Range.intersection(rangeRef.current, range); + } + + Transforms.liftNodes(editor, { + at: range, + match: function match(n) { + return Element$1.isAncestor(node) && node.children.includes(n); + }, + voids: voids + }); + }; + + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + _loop(); + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + if (rangeRef) { + rangeRef.unref(); + } + }); + }, + + /** + * Wrap the nodes at a location in a new container node, splitting the edges + * of the range first to ensure that only the content in the range is wrapped. + */ + wrapNodes: function wrapNodes(editor, element) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode9 = options.mode, + mode = _options$mode9 === void 0 ? 'lowest' : _options$mode9, + _options$split3 = options.split, + split = _options$split3 === void 0 ? false : _options$split3, + _options$voids9 = options.voids, + voids = _options$voids9 === void 0 ? false : _options$voids9; + var match = options.match, + _options$at8 = options.at, + at = _options$at8 === void 0 ? editor.selection : _options$at8; + + if (!at) { + return; + } + + if (match == null) { + if (Path.isPath(at)) { + match = matchPath(editor, at); + } else if (editor.isInline(element)) { + match = function match(n) { + return Editor.isInline(editor, n) || Text.isText(n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + if (split && Range.isRange(at)) { + var _Range$edges7 = Range.edges(at), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; + + var rangeRef = Editor.rangeRef(editor, at, { + affinity: 'inward' + }); + Transforms.splitNodes(editor, { + at: end, + match: match, + voids: voids + }); + Transforms.splitNodes(editor, { + at: start, + match: match, + voids: voids + }); + at = rangeRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + + var roots = Array.from(Editor.nodes(editor, { + at: at, + match: editor.isInline(element) ? function (n) { + return Editor.isBlock(editor, n); + } : function (n) { + return Editor.isEditor(n); + }, + mode: 'lowest', + voids: voids + })); + + for (var _i4 = 0, _roots = roots; _i4 < _roots.length; _i4++) { + var _roots$_i = _slicedToArray(_roots[_i4], 2), + rootPath = _roots$_i[1]; + + var a = Range.isRange(at) ? Range.intersection(at, Editor.range(editor, rootPath)) : at; + + if (!a) { + continue; + } + + var matches = Array.from(Editor.nodes(editor, { + at: a, + match: match, + mode: mode, + voids: voids + })); + + if (matches.length > 0) { + var _ret = function () { + var _matches = _slicedToArray(matches, 1), + first = _matches[0]; + + var last = matches[matches.length - 1]; + + var _first = _slicedToArray(first, 2), + firstPath = _first[1]; + + var _last = _slicedToArray(last, 2), + lastPath = _last[1]; + + if (firstPath.length === 0 && lastPath.length === 0) { + // if there's no matching parent - usually means the node is an editor - don't do anything + return "continue"; + } + + var commonPath = Path.equals(firstPath, lastPath) ? Path.parent(firstPath) : Path.common(firstPath, lastPath); + var range = Editor.range(editor, firstPath, lastPath); + var commonNodeEntry = Editor.node(editor, commonPath); + + var _commonNodeEntry = _slicedToArray(commonNodeEntry, 1), + commonNode = _commonNodeEntry[0]; + + var depth = commonPath.length + 1; + var wrapperPath = Path.next(lastPath.slice(0, depth)); + + var wrapper = _objectSpread$2(_objectSpread$2({}, element), {}, { + children: [] + }); + + Transforms.insertNodes(editor, wrapper, { + at: wrapperPath, + voids: voids + }); + Transforms.moveNodes(editor, { + at: range, + match: function match(n) { + return Element$1.isAncestor(commonNode) && commonNode.children.includes(n); + }, + to: wrapperPath.concat(0), + voids: voids + }); + }(); + + if (_ret === "continue") continue; + } + } + }); + } + }; + + var hasSingleChildNest = function hasSingleChildNest(editor, node) { + if (Element$1.isElement(node)) { + var element = node; + + if (Editor.isVoid(editor, node)) { + return true; + } else if (element.children.length === 1) { + return hasSingleChildNest(editor, element.children[0]); + } else { + return false; + } + } else if (Editor.isEditor(node)) { + return false; + } else { + return true; + } + }; + /** + * Convert a range into a point by deleting it's content. + */ + + + var deleteRange = function deleteRange(editor, range) { + if (Range.isCollapsed(range)) { + return range.anchor; + } else { + var _Range$edges9 = Range.edges(range), + _Range$edges10 = _slicedToArray(_Range$edges9, 2), + end = _Range$edges10[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: range + }); + return pointRef.unref(); + } + }; + + var matchPath = function matchPath(editor, path) { + var _Editor$node5 = Editor.node(editor, path), + _Editor$node6 = _slicedToArray(_Editor$node5, 1), + node = _Editor$node6[0]; + + return function (n) { + return n === node; + }; + }; + + function ownKeys$1(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$1(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$1(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$1(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var SelectionTransforms = { + /** + * Collapse the selection. + */ + collapse: function collapse(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$edge = options.edge, + edge = _options$edge === void 0 ? 'anchor' : _options$edge; + var selection = editor.selection; + + if (!selection) { + return; + } else if (edge === 'anchor') { + Transforms.select(editor, selection.anchor); + } else if (edge === 'focus') { + Transforms.select(editor, selection.focus); + } else if (edge === 'start') { + var _Range$edges = Range.edges(selection), + _Range$edges2 = _slicedToArray(_Range$edges, 1), + start = _Range$edges2[0]; + + Transforms.select(editor, start); + } else if (edge === 'end') { + var _Range$edges3 = Range.edges(selection), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + end = _Range$edges4[1]; + + Transforms.select(editor, end); + } + }, + + /** + * Unset the selection. + */ + deselect: function deselect(editor) { + var selection = editor.selection; + + if (selection) { + editor.apply({ + type: 'set_selection', + properties: selection, + newProperties: null + }); + } + }, + + /** + * Move the selection's point forward or backward. + */ + move: function move(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var selection = editor.selection; + var _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance, + _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit, + _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var _options$edge2 = options.edge, + edge = _options$edge2 === void 0 ? null : _options$edge2; + + if (!selection) { + return; + } + + if (edge === 'start') { + edge = Range.isBackward(selection) ? 'focus' : 'anchor'; + } + + if (edge === 'end') { + edge = Range.isBackward(selection) ? 'anchor' : 'focus'; + } + + var anchor = selection.anchor, + focus = selection.focus; + var opts = { + distance: distance, + unit: unit + }; + var props = {}; + + if (edge == null || edge === 'anchor') { + var point = reverse ? Editor.before(editor, anchor, opts) : Editor.after(editor, anchor, opts); + + if (point) { + props.anchor = point; + } + } + + if (edge == null || edge === 'focus') { + var _point = reverse ? Editor.before(editor, focus, opts) : Editor.after(editor, focus, opts); + + if (_point) { + props.focus = _point; + } + } + + Transforms.setSelection(editor, props); + }, + + /** + * Set the selection to a new value. + */ + select: function select(editor, target) { + var selection = editor.selection; + target = Editor.range(editor, target); + + if (selection) { + Transforms.setSelection(editor, target); + return; + } + + if (!Range.isRange(target)) { + throw new Error("When setting the selection and the current selection is `null` you must provide at least an `anchor` and `focus`, but you passed: ".concat(JSON.stringify(target))); + } + + editor.apply({ + type: 'set_selection', + properties: selection, + newProperties: target + }); + }, + + /** + * Set new properties on one of the selection's points. + */ + setPoint: function setPoint(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var selection = editor.selection; + var _options$edge3 = options.edge, + edge = _options$edge3 === void 0 ? 'both' : _options$edge3; + + if (!selection) { + return; + } + + if (edge === 'start') { + edge = Range.isBackward(selection) ? 'focus' : 'anchor'; + } + + if (edge === 'end') { + edge = Range.isBackward(selection) ? 'anchor' : 'focus'; + } + + var anchor = selection.anchor, + focus = selection.focus; + var point = edge === 'anchor' ? anchor : focus; + Transforms.setSelection(editor, _defineProperty({}, edge === 'anchor' ? 'anchor' : 'focus', _objectSpread$1(_objectSpread$1({}, point), props))); + }, + + /** + * Set new properties on the selection. + */ + setSelection: function setSelection(editor, props) { + var selection = editor.selection; + var oldProps = {}; + var newProps = {}; + + if (!selection) { + return; + } + + for (var k in props) { + if (k === 'anchor' && props.anchor != null && !Point.equals(props.anchor, selection.anchor) || k === 'focus' && props.focus != null && !Point.equals(props.focus, selection.focus) || k !== 'anchor' && k !== 'focus' && props[k] !== selection[k]) { + oldProps[k] = selection[k]; + newProps[k] = props[k]; + } + } + + if (Object.keys(oldProps).length > 0) { + editor.apply({ + type: 'set_selection', + properties: oldProps, + newProperties: newProps + }); + } + } + }; + + function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + + function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var TextTransforms = { + /** + * Delete content in the editor. + */ + "delete": function _delete(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse, + _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit, + _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance, + _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids; + var _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + _options$hanging = options.hanging, + hanging = _options$hanging === void 0 ? false : _options$hanging; + + if (!at) { + return; + } + + if (Range.isRange(at) && Range.isCollapsed(at)) { + at = at.anchor; + } + + if (Point.isPoint(at)) { + var furthestVoid = Editor["void"](editor, { + at: at, + mode: 'highest' + }); + + if (!voids && furthestVoid) { + var _furthestVoid = _slicedToArray(furthestVoid, 2), + voidPath = _furthestVoid[1]; + + at = voidPath; + } else { + var opts = { + unit: unit, + distance: distance + }; + var target = reverse ? Editor.before(editor, at, opts) || Editor.start(editor, []) : Editor.after(editor, at, opts) || Editor.end(editor, []); + at = { + anchor: at, + focus: target + }; + hanging = true; + } + } + + if (Path.isPath(at)) { + Transforms.removeNodes(editor, { + at: at, + voids: voids + }); + return; + } + + if (Range.isCollapsed(at)) { + return; + } + + if (!hanging) { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + _end = _Range$edges2[1]; + + var endOfDoc = Editor.end(editor, []); + + if (!Point.equals(_end, endOfDoc)) { + at = Editor.unhangRange(editor, at, { + voids: voids + }); + } + } + + var _Range$edges3 = Range.edges(at), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + start = _Range$edges4[0], + end = _Range$edges4[1]; + + var startBlock = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: start, + voids: voids + }); + var endBlock = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: end, + voids: voids + }); + var isAcrossBlocks = startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1]); + var isSingleText = Path.equals(start.path, end.path); + var startVoid = voids ? null : Editor["void"](editor, { + at: start, + mode: 'highest' + }); + var endVoid = voids ? null : Editor["void"](editor, { + at: end, + mode: 'highest' + }); // If the start or end points are inside an inline void, nudge them out. + + if (startVoid) { + var before = Editor.before(editor, start); + + if (before && startBlock && Path.isAncestor(startBlock[1], before.path)) { + start = before; + } + } + + if (endVoid) { + var after = Editor.after(editor, end); + + if (after && endBlock && Path.isAncestor(endBlock[1], after.path)) { + end = after; + } + } // Get the highest nodes that are completely inside the range, as well as + // the start and end nodes. + + + var matches = []; + var lastPath; + + var _iterator = _createForOfIteratorHelper(Editor.nodes(editor, { + at: at, + voids: voids + })), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var entry = _step.value; + + var _entry = _slicedToArray(entry, 2), + _node2 = _entry[0], + _path3 = _entry[1]; + + if (lastPath && Path.compare(_path3, lastPath) === 0) { + continue; + } + + if (!voids && Editor.isVoid(editor, _node2) || !Path.isCommon(_path3, start.path) && !Path.isCommon(_path3, end.path)) { + matches.push(entry); + lastPath = _path3; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + var pathRefs = Array.from(matches, function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + p = _ref2[1]; + + return Editor.pathRef(editor, p); + }); + var startRef = Editor.pointRef(editor, start); + var endRef = Editor.pointRef(editor, end); + + if (!isSingleText && !startVoid) { + var _point = startRef.current; + + var _Editor$leaf = Editor.leaf(editor, _point), + _Editor$leaf2 = _slicedToArray(_Editor$leaf, 1), + node = _Editor$leaf2[0]; + + var path = _point.path; + var _start = start, + offset = _start.offset; + var text = node.text.slice(offset); + if (text.length > 0) editor.apply({ + type: 'remove_text', + path: path, + offset: offset, + text: text + }); + } + + for (var _i = 0, _pathRefs = pathRefs; _i < _pathRefs.length; _i++) { + var pathRef = _pathRefs[_i]; + + var _path = pathRef.unref(); + + Transforms.removeNodes(editor, { + at: _path, + voids: voids + }); + } + + if (!endVoid) { + var _point2 = endRef.current; + + var _Editor$leaf3 = Editor.leaf(editor, _point2), + _Editor$leaf4 = _slicedToArray(_Editor$leaf3, 1), + _node = _Editor$leaf4[0]; + + var _path2 = _point2.path; + + var _offset = isSingleText ? start.offset : 0; + + var _text = _node.text.slice(_offset, end.offset); + + if (_text.length > 0) editor.apply({ + type: 'remove_text', + path: _path2, + offset: _offset, + text: _text + }); + } + + if (!isSingleText && isAcrossBlocks && endRef.current && startRef.current) { + Transforms.mergeNodes(editor, { + at: endRef.current, + hanging: true, + voids: voids + }); + } + + var point = reverse ? startRef.unref() || endRef.unref() : endRef.unref() || startRef.unref(); + + if (options.at == null && point) { + Transforms.select(editor, point); + } + }); + }, + + /** + * Insert a fragment at a specific location in the editor. + */ + insertFragment: function insertFragment(editor, fragment) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging2 = options.hanging, + hanging = _options$hanging2 === void 0 ? false : _options$hanging2, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2; + + if (!fragment.length) { + return; + } + + if (!at) { + return; + } else if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges5 = Range.edges(at), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + end = _Range$edges6[1]; + + if (!voids && Editor["void"](editor, { + at: end + })) { + return; + } + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + } + } else if (Path.isPath(at)) { + at = Editor.start(editor, at); + } + + if (!voids && Editor["void"](editor, { + at: at + })) { + return; + } // If the insert point is at the edge of an inline node, move it outside + // instead since it will need to be split otherwise. + + + var inlineElementMatch = Editor.above(editor, { + at: at, + match: function match(n) { + return Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (inlineElementMatch) { + var _inlineElementMatch = _slicedToArray(inlineElementMatch, 2), + _inlinePath = _inlineElementMatch[1]; + + if (Editor.isEnd(editor, at, _inlinePath)) { + var after = Editor.after(editor, _inlinePath); + at = after; + } else if (Editor.isStart(editor, at, _inlinePath)) { + var before = Editor.before(editor, _inlinePath); + at = before; + } + } + + var blockMatch = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: at, + voids: voids + }); + + var _blockMatch = _slicedToArray(blockMatch, 2), + blockPath = _blockMatch[1]; + + var isBlockStart = Editor.isStart(editor, at, blockPath); + var isBlockEnd = Editor.isEnd(editor, at, blockPath); + var isBlockEmpty = isBlockStart && isBlockEnd; + var mergeStart = !isBlockStart || isBlockStart && isBlockEnd; + var mergeEnd = !isBlockEnd; + + var _Node$first = Node$1.first({ + children: fragment + }, []), + _Node$first2 = _slicedToArray(_Node$first, 2), + firstPath = _Node$first2[1]; + + var _Node$last = Node$1.last({ + children: fragment + }, []), + _Node$last2 = _slicedToArray(_Node$last, 2), + lastPath = _Node$last2[1]; + + var matches = []; + + var matcher = function matcher(_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + n = _ref4[0], + p = _ref4[1]; + + var isRoot = p.length === 0; + + if (isRoot) { + return false; + } + + if (isBlockEmpty) { + return true; + } + + if (mergeStart && Path.isAncestor(p, firstPath) && Element$1.isElement(n) && !editor.isVoid(n) && !editor.isInline(n)) { + return false; + } + + if (mergeEnd && Path.isAncestor(p, lastPath) && Element$1.isElement(n) && !editor.isVoid(n) && !editor.isInline(n)) { + return false; + } + + return true; + }; + + var _iterator2 = _createForOfIteratorHelper(Node$1.nodes({ + children: fragment + }, { + pass: matcher + })), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var entry = _step2.value; + + if (matcher(entry)) { + matches.push(entry); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + var starts = []; + var middles = []; + var ends = []; + var starting = true; + var hasBlocks = false; + + for (var _i2 = 0, _matches = matches; _i2 < _matches.length; _i2++) { + var _matches$_i = _slicedToArray(_matches[_i2], 1), + node = _matches$_i[0]; + + if (Element$1.isElement(node) && !editor.isInline(node)) { + starting = false; + hasBlocks = true; + middles.push(node); + } else if (starting) { + starts.push(node); + } else { + ends.push(node); + } + } + + var _Editor$nodes = Editor.nodes(editor, { + at: at, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + inlineMatch = _Editor$nodes2[0]; + + var _inlineMatch = _slicedToArray(inlineMatch, 2), + inlinePath = _inlineMatch[1]; + + var isInlineStart = Editor.isStart(editor, at, inlinePath); + var isInlineEnd = Editor.isEnd(editor, at, inlinePath); + var middleRef = Editor.pathRef(editor, isBlockEnd ? Path.next(blockPath) : blockPath); + var endRef = Editor.pathRef(editor, isInlineEnd ? Path.next(inlinePath) : inlinePath); + var blockPathRef = Editor.pathRef(editor, blockPath); + Transforms.splitNodes(editor, { + at: at, + match: function match(n) { + return hasBlocks ? Editor.isBlock(editor, n) : Text.isText(n) || Editor.isInline(editor, n); + }, + mode: hasBlocks ? 'lowest' : 'highest', + voids: voids + }); + var startRef = Editor.pathRef(editor, !isInlineStart || isInlineStart && isInlineEnd ? Path.next(inlinePath) : inlinePath); + Transforms.insertNodes(editor, starts, { + at: startRef.current, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (isBlockEmpty && middles.length) { + Transforms["delete"](editor, { + at: blockPathRef.unref(), + voids: voids + }); + } + + Transforms.insertNodes(editor, middles, { + at: middleRef.current, + match: function match(n) { + return Editor.isBlock(editor, n); + }, + mode: 'lowest', + voids: voids + }); + Transforms.insertNodes(editor, ends, { + at: endRef.current, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (!options.at) { + var path; + + if (ends.length > 0) { + path = Path.previous(endRef.current); + } else if (middles.length > 0) { + path = Path.previous(middleRef.current); + } else { + path = Path.previous(startRef.current); + } + + var _end2 = Editor.end(editor, path); + + Transforms.select(editor, _end2); + } + + startRef.unref(); + middleRef.unref(); + endRef.unref(); + }); + }, + + /** + * Insert a string of text in the Editor. + */ + insertText: function insertText(editor, text) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3; + var _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3; + + if (!at) { + return; + } + + if (Path.isPath(at)) { + at = Editor.range(editor, at); + } + + if (Range.isRange(at)) { + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var end = Range.end(at); + + if (!voids && Editor["void"](editor, { + at: end + })) { + return; + } + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at, + voids: voids + }); + at = pointRef.unref(); + Transforms.setSelection(editor, { + anchor: at, + focus: at + }); + } + } + + if (!voids && Editor["void"](editor, { + at: at + })) { + return; + } + + var _at = at, + path = _at.path, + offset = _at.offset; + if (text.length > 0) editor.apply({ + type: 'insert_text', + path: path, + offset: offset, + text: text + }); + }); + } + }; + + function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Transforms = _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, GeneralTransforms), NodeTransforms), SelectionTransforms), TextTransforms); + + var Editor_1 = Editor; + var Element_1 = Element$1; + var Location_1 = Location; + var Node_1 = Node$1; + var Operation_1 = Operation; + var Path_1 = Path; + var PathRef_1 = PathRef; + var Point_1 = Point; + var PointRef_1 = PointRef; + var Range_1 = Range; + var RangeRef_1 = RangeRef; + var Span_1 = Span; + var Text_1 = Text; + var Transforms_1 = Transforms; + var createEditor_1 = createEditor$1; + + + var dist$7 = /*#__PURE__*/Object.defineProperty({ + Editor: Editor_1, + Element: Element_1, + Location: Location_1, + Node: Node_1, + Operation: Operation_1, + Path: Path_1, + PathRef: PathRef_1, + Point: Point_1, + PointRef: PointRef_1, + Range: Range_1, + RangeRef: RangeRef_1, + Span: Span_1, + Text: Text_1, + Transforms: Transforms_1, + createEditor: createEditor_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER$1 = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag$1 = '[object Arguments]', + funcTag$1 = '[object Function]', + genTag$1 = '[object GeneratorFunction]', + mapTag = '[object Map]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + setTag = '[object Set]', + stringTag = '[object String]', + weakMapTag = '[object WeakMap]'; + + var dataViewTag = '[object DataView]'; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect unsigned integer values. */ + var reIsUint$1 = /^(?:0|[1-9]\d*)$/; + + /** Used to compose unicode character classes. */ + var rsAstralRange$1 = '\\ud800-\\udfff', + rsComboMarksRange$1 = '\\u0300-\\u036f\\ufe20-\\ufe23', + rsComboSymbolsRange$1 = '\\u20d0-\\u20f0', + rsVarRange$1 = '\\ufe0e\\ufe0f'; + + /** Used to compose unicode capture groups. */ + var rsAstral$1 = '[' + rsAstralRange$1 + ']', + rsCombo$1 = '[' + rsComboMarksRange$1 + rsComboSymbolsRange$1 + ']', + rsFitz$1 = '\\ud83c[\\udffb-\\udfff]', + rsModifier$1 = '(?:' + rsCombo$1 + '|' + rsFitz$1 + ')', + rsNonAstral$1 = '[^' + rsAstralRange$1 + ']', + rsRegional$1 = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair$1 = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsZWJ$1 = '\\u200d'; + + /** Used to compose unicode regexes. */ + var reOptMod$1 = rsModifier$1 + '?', + rsOptVar$1 = '[' + rsVarRange$1 + ']?', + rsOptJoin$1 = '(?:' + rsZWJ$1 + '(?:' + [rsNonAstral$1, rsRegional$1, rsSurrPair$1].join('|') + ')' + rsOptVar$1 + reOptMod$1 + ')*', + rsSeq$1 = rsOptVar$1 + reOptMod$1 + rsOptJoin$1, + rsSymbol$1 = '(?:' + [rsNonAstral$1 + rsCombo$1 + '?', rsCombo$1, rsRegional$1, rsSurrPair$1, rsAstral$1].join('|') + ')'; + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode$1 = RegExp(rsFitz$1 + '(?=' + rsFitz$1 + ')|' + rsSymbol$1 + rsSeq$1, 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode$1 = RegExp('[' + rsZWJ$1 + rsAstralRange$1 + rsComboMarksRange$1 + rsComboSymbolsRange$1 + rsVarRange$1 + ']'); + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$3 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$3 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$3 = freeGlobal$3 || freeSelf$3 || Function('return this')(); + + /** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array ? array.length : 0, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray$1(string) { + return string.split(''); + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes$1(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * The base implementation of `_.values` and `_.valuesIn` which creates an + * array of `object` property values corresponding to the property names + * of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the array of property values. + */ + function baseValues(object, props) { + return arrayMap(props, function(key) { + return object[key]; + }); + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode$1(string) { + return reHasUnicode$1.test(string); + } + + /** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ + function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; + } + + /** + * Converts `iterator` to an array. + * + * @private + * @param {Object} iterator The iterator to convert. + * @returns {Array} Returns the converted array. + */ + function iteratorToArray(iterator) { + var data, + result = []; + + while (!(data = iterator.next()).done) { + result.push(data.value); + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg$1(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray$1(string) { + return hasUnicode$1(string) + ? unicodeToArray$1(string) + : asciiToArray$1(string); + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray$1(string) { + return string.match(reUnicode$1) || []; + } + + /** Used for built-in method references. */ + var funcProto = Function.prototype, + objectProto$4 = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = root$3['__core-js_shared__']; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty$2 = objectProto$4.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$4 = objectProto$4.toString; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty$2).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Symbol$2 = root$3.Symbol, + iteratorSymbol = Symbol$2 ? Symbol$2.iterator : undefined, + propertyIsEnumerable$1 = objectProto$4.propertyIsEnumerable; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeKeys$1 = overArg$1(Object.keys, Object); + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(root$3, 'DataView'), + Map$1 = getNative(root$3, 'Map'), + Promise$1 = getNative(root$3, 'Promise'), + Set$1 = getNative(root$3, 'Set'), + WeakMap$1 = getNative(root$3, 'WeakMap'); + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map$1), + promiseCtorString = toSource(Promise$1), + setCtorString = toSource(Set$1), + weakMapCtorString = toSource(WeakMap$1); + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys$1(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray$1(value) || isArguments$1(value)) + ? baseTimes$1(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty$2.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex$1(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + return objectToString$4.call(value); + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject$3(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction$1(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys$1(object) { + if (!isPrototype$1(object)) { + return nativeKeys$1(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty$2.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11, + // for data views in Edge < 14, and promises in Node.js. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map$1 && getTag(new Map$1) != mapTag) || + (Promise$1 && getTag(Promise$1.resolve()) != promiseTag) || + (Set$1 && getTag(new Set$1) != setTag) || + (WeakMap$1 && getTag(new WeakMap$1) != weakMapTag)) { + getTag = function(value) { + var result = objectToString$4.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex$1(value, length) { + length = length == null ? MAX_SAFE_INTEGER$1 : length; + return !!length && + (typeof value == 'number' || reIsUint$1.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype$1(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$4; + + return value === proto; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments$1(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject$1(value) && hasOwnProperty$2.call(value, 'callee') && + (!propertyIsEnumerable$1.call(value, 'callee') || objectToString$4.call(value) == argsTag$1); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray$1 = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike$1(value) { + return value != null && isLength$1(value.length) && !isFunction$1(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject$1(value) { + return isObjectLike$4(value) && isArrayLike$1(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction$1(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject$3(value) ? objectToString$4.call(value) : ''; + return tag == funcTag$1 || tag == genTag$1; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength$1(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER$1; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$3(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$4(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ + function isString(value) { + return typeof value == 'string' || + (!isArray$1(value) && isObjectLike$4(value) && objectToString$4.call(value) == stringTag); + } + + /** + * Converts `value` to an array. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Array} Returns the converted array. + * @example + * + * _.toArray({ 'a': 1, 'b': 2 }); + * // => [1, 2] + * + * _.toArray('abc'); + * // => ['a', 'b', 'c'] + * + * _.toArray(1); + * // => [] + * + * _.toArray(null); + * // => [] + */ + function toArray(value) { + if (!value) { + return []; + } + if (isArrayLike$1(value)) { + return isString(value) ? stringToArray$1(value) : copyArray(value); + } + if (iteratorSymbol && value[iteratorSymbol]) { + return iteratorToArray(value[iteratorSymbol]()); + } + var tag = getTag(value), + func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); + + return func(value); + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys$1(object) { + return isArrayLike$1(object) ? arrayLikeKeys$1(object) : baseKeys$1(object); + } + + /** + * Creates an array of the own enumerable string keyed property values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.values(new Foo); + * // => [1, 2] (iteration order is not guaranteed) + * + * _.values('hi'); + * // => ['h', 'i'] + */ + function values(object) { + return object ? baseValues(object, keys$1(object)) : []; + } + + var lodash_toarray = toArray; + + /** + * SSR Window 3.0.0 + * Better handling for window object in SSR environment + * https://github.com/nolimits4web/ssr-window + * + * Copyright 2020, Vladimir Kharlampidi + * + * Licensed under MIT + * + * Released on: November 9, 2020 + */ + + var ssrWindow_umd = createCommonjsModule$1(function (module, exports) { + (function (global, factory) { + factory(exports) ; + }(commonjsGlobal, (function (exports) { + /* eslint-disable no-param-reassign */ + function isObject(obj) { + return (obj !== null && + typeof obj === 'object' && + 'constructor' in obj && + obj.constructor === Object); + } + function extend(target, src) { + if (target === void 0) { target = {}; } + if (src === void 0) { src = {}; } + Object.keys(src).forEach(function (key) { + if (typeof target[key] === 'undefined') + target[key] = src[key]; + else if (isObject(src[key]) && + isObject(target[key]) && + Object.keys(src[key]).length > 0) { + extend(target[key], src[key]); + } + }); + } + + var ssrDocument = { + body: {}, + addEventListener: function () { }, + removeEventListener: function () { }, + activeElement: { + blur: function () { }, + nodeName: '', + }, + querySelector: function () { + return null; + }, + querySelectorAll: function () { + return []; + }, + getElementById: function () { + return null; + }, + createEvent: function () { + return { + initEvent: function () { }, + }; + }, + createElement: function () { + return { + children: [], + childNodes: [], + style: {}, + setAttribute: function () { }, + getElementsByTagName: function () { + return []; + }, + }; + }, + createElementNS: function () { + return {}; + }, + importNode: function () { + return null; + }, + location: { + hash: '', + host: '', + hostname: '', + href: '', + origin: '', + pathname: '', + protocol: '', + search: '', + }, + }; + function getDocument() { + var doc = typeof document !== 'undefined' ? document : {}; + extend(doc, ssrDocument); + return doc; + } + + var ssrWindow = { + document: ssrDocument, + navigator: { + userAgent: '', + }, + location: { + hash: '', + host: '', + hostname: '', + href: '', + origin: '', + pathname: '', + protocol: '', + search: '', + }, + history: { + replaceState: function () { }, + pushState: function () { }, + go: function () { }, + back: function () { }, + }, + CustomEvent: function CustomEvent() { + return this; + }, + addEventListener: function () { }, + removeEventListener: function () { }, + getComputedStyle: function () { + return { + getPropertyValue: function () { + return ''; + }, + }; + }, + Image: function () { }, + Date: function () { }, + screen: {}, + setTimeout: function () { }, + clearTimeout: function () { }, + matchMedia: function () { + return {}; + }, + requestAnimationFrame: function (callback) { + if (typeof setTimeout === 'undefined') { + callback(); + return null; + } + return setTimeout(callback, 0); + }, + cancelAnimationFrame: function (id) { + if (typeof setTimeout === 'undefined') { + return; + } + clearTimeout(id); + }, + }; + function getWindow() { + var win = typeof window !== 'undefined' ? window : {}; + extend(win, ssrWindow); + return win; + } + + exports.extend = extend; + exports.getDocument = getDocument; + exports.getWindow = getWindow; + exports.ssrDocument = ssrDocument; + exports.ssrWindow = ssrWindow; + + Object.defineProperty(exports, '__esModule', { value: true }); + + }))); + + }); + + /** + * Dom7 3.0.0 + * Minimalistic JavaScript library for DOM manipulation, with a jQuery-compatible API + * https://framework7.io/docs/dom7.html + * + * Copyright 2020, Vladimir Kharlampidi + * + * Licensed under MIT + * + * Released on: November 9, 2020 + */ + + + + + + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct; + } else { + _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) _setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); + } + + function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; + } + + function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? new Map() : undefined; + + _wrapNativeSuper = function _wrapNativeSuper(Class) { + if (Class === null || !_isNativeFunction(Class)) return Class; + + if (typeof Class !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + + if (typeof _cache !== "undefined") { + if (_cache.has(Class)) return _cache.get(Class); + + _cache.set(Class, Wrapper); + } + + function Wrapper() { + return _construct(Class, arguments, _getPrototypeOf(this).constructor); + } + + Wrapper.prototype = Object.create(Class.prototype, { + constructor: { + value: Wrapper, + enumerable: false, + writable: true, + configurable: true + } + }); + return _setPrototypeOf(Wrapper, Class); + }; + + return _wrapNativeSuper(Class); + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + /* eslint-disable no-proto */ + function makeReactive(obj) { + var proto = obj.__proto__; + Object.defineProperty(obj, '__proto__', { + get: function get() { + return proto; + }, + set: function set(value) { + proto.__proto__ = value; + } + }); + } + + var Dom7 = /*#__PURE__*/function (_Array) { + _inheritsLoose(Dom7, _Array); + + function Dom7(items) { + var _this; + + _this = _Array.call.apply(_Array, [this].concat(items)) || this; + makeReactive(_assertThisInitialized(_this)); + return _this; + } + + return Dom7; + }( /*#__PURE__*/_wrapNativeSuper(Array)); + + function arrayFlat(arr) { + if (arr === void 0) { + arr = []; + } + + var res = []; + arr.forEach(function (el) { + if (Array.isArray(el)) { + res.push.apply(res, arrayFlat(el)); + } else { + res.push(el); + } + }); + return res; + } + function arrayFilter(arr, callback) { + return Array.prototype.filter.call(arr, callback); + } + function arrayUnique(arr) { + var uniqueArray = []; + + for (var i = 0; i < arr.length; i += 1) { + if (uniqueArray.indexOf(arr[i]) === -1) uniqueArray.push(arr[i]); + } + + return uniqueArray; + } + function toCamelCase(string) { + return string.toLowerCase().replace(/-(.)/g, function (match, group) { + return group.toUpperCase(); + }); + } + + function qsa(selector, context) { + if (typeof selector !== 'string') { + return [selector]; + } + + var a = []; + var res = context.querySelectorAll(selector); + + for (var i = 0; i < res.length; i += 1) { + a.push(res[i]); + } + + return a; + } + + function $(selector, context) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var arr = []; + + if (!context && selector instanceof Dom7) { + return selector; + } + + if (!selector) { + return new Dom7(arr); + } + + if (typeof selector === 'string') { + var html = selector.trim(); + + if (html.indexOf('<') >= 0 && html.indexOf('>') >= 0) { + var toCreate = 'div'; + if (html.indexOf(' 0; + }).length > 0; + } + + function attr(attrs, value) { + if (arguments.length === 1 && typeof attrs === 'string') { + // Get attr + if (this[0]) return this[0].getAttribute(attrs); + return undefined; + } // Set attrs + + + for (var i = 0; i < this.length; i += 1) { + if (arguments.length === 2) { + // String + this[i].setAttribute(attrs, value); + } else { + // Object + for (var attrName in attrs) { + this[i][attrName] = attrs[attrName]; + this[i].setAttribute(attrName, attrs[attrName]); + } + } + } + + return this; + } + + function removeAttr(attr) { + for (var i = 0; i < this.length; i += 1) { + this[i].removeAttribute(attr); + } + + return this; + } + + function prop(props, value) { + if (arguments.length === 1 && typeof props === 'string') { + // Get prop + if (this[0]) return this[0][props]; + } else { + // Set props + for (var i = 0; i < this.length; i += 1) { + if (arguments.length === 2) { + // String + this[i][props] = value; + } else { + // Object + for (var propName in props) { + this[i][propName] = props[propName]; + } + } + } + + return this; + } + + return this; + } + + function data(key, value) { + var el; + + if (typeof value === 'undefined') { + el = this[0]; + if (!el) return undefined; // Get value + + if (el.dom7ElementDataStorage && key in el.dom7ElementDataStorage) { + return el.dom7ElementDataStorage[key]; + } + + var dataKey = el.getAttribute("data-" + key); + + if (dataKey) { + return dataKey; + } + + return undefined; + } // Set value + + + for (var i = 0; i < this.length; i += 1) { + el = this[i]; + if (!el.dom7ElementDataStorage) el.dom7ElementDataStorage = {}; + el.dom7ElementDataStorage[key] = value; + } + + return this; + } + + function removeData(key) { + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.dom7ElementDataStorage && el.dom7ElementDataStorage[key]) { + el.dom7ElementDataStorage[key] = null; + delete el.dom7ElementDataStorage[key]; + } + } + } + + function dataset() { + var el = this[0]; + if (!el) return undefined; + var dataset = {}; // eslint-disable-line + + if (el.dataset) { + for (var dataKey in el.dataset) { + dataset[dataKey] = el.dataset[dataKey]; + } + } else { + for (var i = 0; i < el.attributes.length; i += 1) { + var _attr = el.attributes[i]; + + if (_attr.name.indexOf('data-') >= 0) { + dataset[toCamelCase(_attr.name.split('data-')[1])] = _attr.value; + } + } + } + + for (var key in dataset) { + if (dataset[key] === 'false') dataset[key] = false;else if (dataset[key] === 'true') dataset[key] = true;else if (parseFloat(dataset[key]) === dataset[key] * 1) dataset[key] *= 1; + } + + return dataset; + } + + function val(value) { + if (typeof value === 'undefined') { + // get value + var el = this[0]; + if (!el) return undefined; + + if (el.multiple && el.nodeName.toLowerCase() === 'select') { + var values = []; + + for (var i = 0; i < el.selectedOptions.length; i += 1) { + values.push(el.selectedOptions[i].value); + } + + return values; + } + + return el.value; + } // set value + + + for (var _i = 0; _i < this.length; _i += 1) { + var _el = this[_i]; + + if (Array.isArray(value) && _el.multiple && _el.nodeName.toLowerCase() === 'select') { + for (var j = 0; j < _el.options.length; j += 1) { + _el.options[j].selected = value.indexOf(_el.options[j].value) >= 0; + } + } else { + _el.value = value; + } + } + + return this; + } + + function value(value) { + return this.val(value); + } + + function transform(transform) { + for (var i = 0; i < this.length; i += 1) { + this[i].style.transform = transform; + } + + return this; + } + + function transition(duration) { + for (var i = 0; i < this.length; i += 1) { + this[i].style.transitionDuration = typeof duration !== 'string' ? duration + "ms" : duration; + } + + return this; + } + + function on() { + for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + args[_key5] = arguments[_key5]; + } + + var eventType = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventType = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + if (!capture) capture = false; + + function handleLiveEvent(e) { + var target = e.target; + if (!target) return; + var eventData = e.target.dom7EventData || []; + + if (eventData.indexOf(e) < 0) { + eventData.unshift(e); + } + + if ($(target).is(targetSelector)) listener.apply(target, eventData);else { + var _parents = $(target).parents(); // eslint-disable-line + + + for (var k = 0; k < _parents.length; k += 1) { + if ($(_parents[k]).is(targetSelector)) listener.apply(_parents[k], eventData); + } + } + } + + function handleEvent(e) { + var eventData = e && e.target ? e.target.dom7EventData || [] : []; + + if (eventData.indexOf(e) < 0) { + eventData.unshift(e); + } + + listener.apply(this, eventData); + } + + var events = eventType.split(' '); + var j; + + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (!targetSelector) { + for (j = 0; j < events.length; j += 1) { + var event = events[j]; + if (!el.dom7Listeners) el.dom7Listeners = {}; + if (!el.dom7Listeners[event]) el.dom7Listeners[event] = []; + el.dom7Listeners[event].push({ + listener: listener, + proxyListener: handleEvent + }); + el.addEventListener(event, handleEvent, capture); + } + } else { + // Live events + for (j = 0; j < events.length; j += 1) { + var _event = events[j]; + if (!el.dom7LiveListeners) el.dom7LiveListeners = {}; + if (!el.dom7LiveListeners[_event]) el.dom7LiveListeners[_event] = []; + + el.dom7LiveListeners[_event].push({ + listener: listener, + proxyListener: handleLiveEvent + }); + + el.addEventListener(_event, handleLiveEvent, capture); + } + } + } + + return this; + } + + function off() { + for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { + args[_key6] = arguments[_key6]; + } + + var eventType = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventType = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + if (!capture) capture = false; + var events = eventType.split(' '); + + for (var i = 0; i < events.length; i += 1) { + var event = events[i]; + + for (var j = 0; j < this.length; j += 1) { + var el = this[j]; + var handlers = void 0; + + if (!targetSelector && el.dom7Listeners) { + handlers = el.dom7Listeners[event]; + } else if (targetSelector && el.dom7LiveListeners) { + handlers = el.dom7LiveListeners[event]; + } + + if (handlers && handlers.length) { + for (var k = handlers.length - 1; k >= 0; k -= 1) { + var handler = handlers[k]; + + if (listener && handler.listener === listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } else if (listener && handler.listener && handler.listener.dom7proxy && handler.listener.dom7proxy === listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } else if (!listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } + } + } + } + } + + return this; + } + + function once() { + var dom = this; + + for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { + args[_key7] = arguments[_key7]; + } + + var eventName = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventName = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + function onceHandler() { + for (var _len8 = arguments.length, eventArgs = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { + eventArgs[_key8] = arguments[_key8]; + } + + listener.apply(this, eventArgs); + dom.off(eventName, targetSelector, onceHandler, capture); + + if (onceHandler.dom7proxy) { + delete onceHandler.dom7proxy; + } + } + + onceHandler.dom7proxy = listener; + return dom.on(eventName, targetSelector, onceHandler, capture); + } + + function trigger() { + var window = ssrWindow_umd.getWindow(); + + for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) { + args[_key9] = arguments[_key9]; + } + + var events = args[0].split(' '); + var eventData = args[1]; + + for (var i = 0; i < events.length; i += 1) { + var event = events[i]; + + for (var j = 0; j < this.length; j += 1) { + var el = this[j]; + + if (window.CustomEvent) { + var evt = new window.CustomEvent(event, { + detail: eventData, + bubbles: true, + cancelable: true + }); + el.dom7EventData = args.filter(function (data, dataIndex) { + return dataIndex > 0; + }); + el.dispatchEvent(evt); + el.dom7EventData = []; + delete el.dom7EventData; + } + } + } + + return this; + } + + function transitionEnd(callback) { + var dom = this; + + function fireCallBack(e) { + if (e.target !== this) return; + callback.call(this, e); + dom.off('transitionend', fireCallBack); + } + + if (callback) { + dom.on('transitionend', fireCallBack); + } + + return this; + } + + function animationEnd(callback) { + var dom = this; + + function fireCallBack(e) { + if (e.target !== this) return; + callback.call(this, e); + dom.off('animationend', fireCallBack); + } + + if (callback) { + dom.on('animationend', fireCallBack); + } + + return this; + } + + function width() { + var window = ssrWindow_umd.getWindow(); + + if (this[0] === window) { + return window.innerWidth; + } + + if (this.length > 0) { + return parseFloat(this.css('width')); + } + + return null; + } + + function outerWidth(includeMargins) { + if (this.length > 0) { + if (includeMargins) { + var _styles = this.styles(); + + return this[0].offsetWidth + parseFloat(_styles.getPropertyValue('margin-right')) + parseFloat(_styles.getPropertyValue('margin-left')); + } + + return this[0].offsetWidth; + } + + return null; + } + + function height() { + var window = ssrWindow_umd.getWindow(); + + if (this[0] === window) { + return window.innerHeight; + } + + if (this.length > 0) { + return parseFloat(this.css('height')); + } + + return null; + } + + function outerHeight(includeMargins) { + if (this.length > 0) { + if (includeMargins) { + var _styles2 = this.styles(); + + return this[0].offsetHeight + parseFloat(_styles2.getPropertyValue('margin-top')) + parseFloat(_styles2.getPropertyValue('margin-bottom')); + } + + return this[0].offsetHeight; + } + + return null; + } + + function offset() { + if (this.length > 0) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var el = this[0]; + var box = el.getBoundingClientRect(); + var body = document.body; + var clientTop = el.clientTop || body.clientTop || 0; + var clientLeft = el.clientLeft || body.clientLeft || 0; + var scrollTop = el === window ? window.scrollY : el.scrollTop; + var scrollLeft = el === window ? window.scrollX : el.scrollLeft; + return { + top: box.top + scrollTop - clientTop, + left: box.left + scrollLeft - clientLeft + }; + } + + return null; + } + + function hide() { + for (var i = 0; i < this.length; i += 1) { + this[i].style.display = 'none'; + } + + return this; + } + + function show() { + var window = ssrWindow_umd.getWindow(); + + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.style.display === 'none') { + el.style.display = ''; + } + + if (window.getComputedStyle(el, null).getPropertyValue('display') === 'none') { + // Still not visible + el.style.display = 'block'; + } + } + + return this; + } + + function styles() { + var window = ssrWindow_umd.getWindow(); + if (this[0]) return window.getComputedStyle(this[0], null); + return {}; + } + + function css(props, value) { + var window = ssrWindow_umd.getWindow(); + var i; + + if (arguments.length === 1) { + if (typeof props === 'string') { + // .css('width') + if (this[0]) return window.getComputedStyle(this[0], null).getPropertyValue(props); + } else { + // .css({ width: '100px' }) + for (i = 0; i < this.length; i += 1) { + for (var _prop in props) { + this[i].style[_prop] = props[_prop]; + } + } + + return this; + } + } + + if (arguments.length === 2 && typeof props === 'string') { + // .css('width', '100px') + for (i = 0; i < this.length; i += 1) { + this[i].style[props] = value; + } + + return this; + } + + return this; + } + + function each(callback) { + if (!callback) return this; + this.forEach(function (el, index) { + callback.apply(el, [el, index]); + }); + return this; + } + + function filter(callback) { + var result = arrayFilter(this, callback); + return $(result); + } + + function html(html) { + if (typeof html === 'undefined') { + return this[0] ? this[0].innerHTML : null; + } + + for (var i = 0; i < this.length; i += 1) { + this[i].innerHTML = html; + } + + return this; + } + + function text(text) { + if (typeof text === 'undefined') { + return this[0] ? this[0].textContent.trim() : null; + } + + for (var i = 0; i < this.length; i += 1) { + this[i].textContent = text; + } + + return this; + } + + function is(selector) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var el = this[0]; + var compareWith; + var i; + if (!el || typeof selector === 'undefined') return false; + + if (typeof selector === 'string') { + if (el.matches) return el.matches(selector); + if (el.webkitMatchesSelector) return el.webkitMatchesSelector(selector); + if (el.msMatchesSelector) return el.msMatchesSelector(selector); + compareWith = $(selector); + + for (i = 0; i < compareWith.length; i += 1) { + if (compareWith[i] === el) return true; + } + + return false; + } + + if (selector === document) { + return el === document; + } + + if (selector === window) { + return el === window; + } + + if (selector.nodeType || selector instanceof Dom7) { + compareWith = selector.nodeType ? [selector] : selector; + + for (i = 0; i < compareWith.length; i += 1) { + if (compareWith[i] === el) return true; + } + + return false; + } + + return false; + } + + function index$1() { + var child = this[0]; + var i; + + if (child) { + i = 0; // eslint-disable-next-line + + while ((child = child.previousSibling) !== null) { + if (child.nodeType === 1) i += 1; + } + + return i; + } + + return undefined; + } + + function eq(index) { + if (typeof index === 'undefined') return this; + var length = this.length; + + if (index > length - 1) { + return $([]); + } + + if (index < 0) { + var returnIndex = length + index; + if (returnIndex < 0) return $([]); + return $([this[returnIndex]]); + } + + return $([this[index]]); + } + + function append() { + var newChild; + var document = ssrWindow_umd.getDocument(); + + for (var k = 0; k < arguments.length; k += 1) { + newChild = k < 0 || arguments.length <= k ? undefined : arguments[k]; + + for (var i = 0; i < this.length; i += 1) { + if (typeof newChild === 'string') { + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = newChild; + + while (tempDiv.firstChild) { + this[i].appendChild(tempDiv.firstChild); + } + } else if (newChild instanceof Dom7) { + for (var j = 0; j < newChild.length; j += 1) { + this[i].appendChild(newChild[j]); + } + } else { + this[i].appendChild(newChild); + } + } + } + + return this; + } + + function appendTo(parent) { + $(parent).append(this); + return this; + } + + function prepend(newChild) { + var document = ssrWindow_umd.getDocument(); + var i; + var j; + + for (i = 0; i < this.length; i += 1) { + if (typeof newChild === 'string') { + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = newChild; + + for (j = tempDiv.childNodes.length - 1; j >= 0; j -= 1) { + this[i].insertBefore(tempDiv.childNodes[j], this[i].childNodes[0]); + } + } else if (newChild instanceof Dom7) { + for (j = 0; j < newChild.length; j += 1) { + this[i].insertBefore(newChild[j], this[i].childNodes[0]); + } + } else { + this[i].insertBefore(newChild, this[i].childNodes[0]); + } + } + + return this; + } + + function prependTo(parent) { + $(parent).prepend(this); + return this; + } + + function insertBefore(selector) { + var before = $(selector); + + for (var i = 0; i < this.length; i += 1) { + if (before.length === 1) { + before[0].parentNode.insertBefore(this[i], before[0]); + } else if (before.length > 1) { + for (var j = 0; j < before.length; j += 1) { + before[j].parentNode.insertBefore(this[i].cloneNode(true), before[j]); + } + } + } + } + + function insertAfter(selector) { + var after = $(selector); + + for (var i = 0; i < this.length; i += 1) { + if (after.length === 1) { + after[0].parentNode.insertBefore(this[i], after[0].nextSibling); + } else if (after.length > 1) { + for (var j = 0; j < after.length; j += 1) { + after[j].parentNode.insertBefore(this[i].cloneNode(true), after[j].nextSibling); + } + } + } + } + + function next(selector) { + if (this.length > 0) { + if (selector) { + if (this[0].nextElementSibling && $(this[0].nextElementSibling).is(selector)) { + return $([this[0].nextElementSibling]); + } + + return $([]); + } + + if (this[0].nextElementSibling) return $([this[0].nextElementSibling]); + return $([]); + } + + return $([]); + } + + function nextAll(selector) { + var nextEls = []; + var el = this[0]; + if (!el) return $([]); + + while (el.nextElementSibling) { + var _next = el.nextElementSibling; // eslint-disable-line + + if (selector) { + if ($(_next).is(selector)) nextEls.push(_next); + } else nextEls.push(_next); + + el = _next; + } + + return $(nextEls); + } + + function prev(selector) { + if (this.length > 0) { + var el = this[0]; + + if (selector) { + if (el.previousElementSibling && $(el.previousElementSibling).is(selector)) { + return $([el.previousElementSibling]); + } + + return $([]); + } + + if (el.previousElementSibling) return $([el.previousElementSibling]); + return $([]); + } + + return $([]); + } + + function prevAll(selector) { + var prevEls = []; + var el = this[0]; + if (!el) return $([]); + + while (el.previousElementSibling) { + var _prev = el.previousElementSibling; // eslint-disable-line + + if (selector) { + if ($(_prev).is(selector)) prevEls.push(_prev); + } else prevEls.push(_prev); + + el = _prev; + } + + return $(prevEls); + } + + function siblings(selector) { + return this.nextAll(selector).add(this.prevAll(selector)); + } + + function parent(selector) { + var parents = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + if (this[i].parentNode !== null) { + if (selector) { + if ($(this[i].parentNode).is(selector)) parents.push(this[i].parentNode); + } else { + parents.push(this[i].parentNode); + } + } + } + + return $(parents); + } + + function parents(selector) { + var parents = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + var _parent = this[i].parentNode; // eslint-disable-line + + while (_parent) { + if (selector) { + if ($(_parent).is(selector)) parents.push(_parent); + } else { + parents.push(_parent); + } + + _parent = _parent.parentNode; + } + } + + return $(parents); + } + + function closest(selector) { + var closest = this; // eslint-disable-line + + if (typeof selector === 'undefined') { + return $([]); + } + + if (!closest.is(selector)) { + closest = closest.parents(selector).eq(0); + } + + return closest; + } + + function find(selector) { + var foundElements = []; + + for (var i = 0; i < this.length; i += 1) { + var found = this[i].querySelectorAll(selector); + + for (var j = 0; j < found.length; j += 1) { + foundElements.push(found[j]); + } + } + + return $(foundElements); + } + + function children(selector) { + var children = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + var childNodes = this[i].children; + + for (var j = 0; j < childNodes.length; j += 1) { + if (!selector || $(childNodes[j]).is(selector)) { + children.push(childNodes[j]); + } + } + } + + return $(children); + } + + function remove() { + for (var i = 0; i < this.length; i += 1) { + if (this[i].parentNode) this[i].parentNode.removeChild(this[i]); + } + + return this; + } + + function detach() { + return this.remove(); + } + + function add() { + var dom = this; + var i; + var j; + + for (var _len10 = arguments.length, els = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) { + els[_key10] = arguments[_key10]; + } + + for (i = 0; i < els.length; i += 1) { + var toAdd = $(els[i]); + + for (j = 0; j < toAdd.length; j += 1) { + dom.push(toAdd[j]); + } + } + + return dom; + } + + function empty() { + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.nodeType === 1) { + for (var j = 0; j < el.childNodes.length; j += 1) { + if (el.childNodes[j].parentNode) { + el.childNodes[j].parentNode.removeChild(el.childNodes[j]); + } + } + + el.textContent = ''; + } + } + + return this; + } + + function scrollTo() { + var window = ssrWindow_umd.getWindow(); + + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var left = args[0], + top = args[1], + duration = args[2], + easing = args[3], + callback = args[4]; + + if (args.length === 4 && typeof easing === 'function') { + callback = easing; + left = args[0]; + top = args[1]; + duration = args[2]; + callback = args[3]; + easing = args[4]; + } + + if (typeof easing === 'undefined') easing = 'swing'; + return this.each(function animate() { + var el = this; + var currentTop; + var currentLeft; + var maxTop; + var maxLeft; + var newTop; + var newLeft; + var scrollTop; // eslint-disable-line + + var scrollLeft; // eslint-disable-line + + var animateTop = top > 0 || top === 0; + var animateLeft = left > 0 || left === 0; + + if (typeof easing === 'undefined') { + easing = 'swing'; + } + + if (animateTop) { + currentTop = el.scrollTop; + + if (!duration) { + el.scrollTop = top; + } + } + + if (animateLeft) { + currentLeft = el.scrollLeft; + + if (!duration) { + el.scrollLeft = left; + } + } + + if (!duration) return; + + if (animateTop) { + maxTop = el.scrollHeight - el.offsetHeight; + newTop = Math.max(Math.min(top, maxTop), 0); + } + + if (animateLeft) { + maxLeft = el.scrollWidth - el.offsetWidth; + newLeft = Math.max(Math.min(left, maxLeft), 0); + } + + var startTime = null; + if (animateTop && newTop === currentTop) animateTop = false; + if (animateLeft && newLeft === currentLeft) animateLeft = false; + + function render(time) { + if (time === void 0) { + time = new Date().getTime(); + } + + if (startTime === null) { + startTime = time; + } + + var progress = Math.max(Math.min((time - startTime) / duration, 1), 0); + var easeProgress = easing === 'linear' ? progress : 0.5 - Math.cos(progress * Math.PI) / 2; + var done; + if (animateTop) scrollTop = currentTop + easeProgress * (newTop - currentTop); + if (animateLeft) scrollLeft = currentLeft + easeProgress * (newLeft - currentLeft); + + if (animateTop && newTop > currentTop && scrollTop >= newTop) { + el.scrollTop = newTop; + done = true; + } + + if (animateTop && newTop < currentTop && scrollTop <= newTop) { + el.scrollTop = newTop; + done = true; + } + + if (animateLeft && newLeft > currentLeft && scrollLeft >= newLeft) { + el.scrollLeft = newLeft; + done = true; + } + + if (animateLeft && newLeft < currentLeft && scrollLeft <= newLeft) { + el.scrollLeft = newLeft; + done = true; + } + + if (done) { + if (callback) callback(); + return; + } + + if (animateTop) el.scrollTop = scrollTop; + if (animateLeft) el.scrollLeft = scrollLeft; + window.requestAnimationFrame(render); + } + + window.requestAnimationFrame(render); + }); + } // scrollTop(top, duration, easing, callback) { + + + function scrollTop() { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var top = args[0], + duration = args[1], + easing = args[2], + callback = args[3]; + + if (args.length === 3 && typeof easing === 'function') { + top = args[0]; + duration = args[1]; + callback = args[2]; + easing = args[3]; + } + + var dom = this; + + if (typeof top === 'undefined') { + if (dom.length > 0) return dom[0].scrollTop; + return null; + } + + return dom.scrollTo(undefined, top, duration, easing, callback); + } + + function scrollLeft() { + for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + var left = args[0], + duration = args[1], + easing = args[2], + callback = args[3]; + + if (args.length === 3 && typeof easing === 'function') { + left = args[0]; + duration = args[1]; + callback = args[2]; + easing = args[3]; + } + + var dom = this; + + if (typeof left === 'undefined') { + if (dom.length > 0) return dom[0].scrollLeft; + return null; + } + + return dom.scrollTo(left, undefined, duration, easing, callback); + } + + function animate(initialProps, initialParams) { + var window = ssrWindow_umd.getWindow(); + var els = this; + var a = { + props: Object.assign({}, initialProps), + params: Object.assign({ + duration: 300, + easing: 'swing' // or 'linear' + + /* Callbacks + begin(elements) + complete(elements) + progress(elements, complete, remaining, start, tweenValue) + */ + + }, initialParams), + elements: els, + animating: false, + que: [], + easingProgress: function easingProgress(easing, progress) { + if (easing === 'swing') { + return 0.5 - Math.cos(progress * Math.PI) / 2; + } + + if (typeof easing === 'function') { + return easing(progress); + } + + return progress; + }, + stop: function stop() { + if (a.frameId) { + window.cancelAnimationFrame(a.frameId); + } + + a.animating = false; + a.elements.each(function (el) { + var element = el; + delete element.dom7AnimateInstance; + }); + a.que = []; + }, + done: function done(complete) { + a.animating = false; + a.elements.each(function (el) { + var element = el; + delete element.dom7AnimateInstance; + }); + if (complete) complete(els); + + if (a.que.length > 0) { + var que = a.que.shift(); + a.animate(que[0], que[1]); + } + }, + animate: function animate(props, params) { + if (a.animating) { + a.que.push([props, params]); + return a; + } + + var elements = []; // Define & Cache Initials & Units + + a.elements.each(function (el, index) { + var initialFullValue; + var initialValue; + var unit; + var finalValue; + var finalFullValue; + if (!el.dom7AnimateInstance) a.elements[index].dom7AnimateInstance = a; + elements[index] = { + container: el + }; + Object.keys(props).forEach(function (prop) { + initialFullValue = window.getComputedStyle(el, null).getPropertyValue(prop).replace(',', '.'); + initialValue = parseFloat(initialFullValue); + unit = initialFullValue.replace(initialValue, ''); + finalValue = parseFloat(props[prop]); + finalFullValue = props[prop] + unit; + elements[index][prop] = { + initialFullValue: initialFullValue, + initialValue: initialValue, + unit: unit, + finalValue: finalValue, + finalFullValue: finalFullValue, + currentValue: initialValue + }; + }); + }); + var startTime = null; + var time; + var elementsDone = 0; + var propsDone = 0; + var done; + var began = false; + a.animating = true; + + function render() { + time = new Date().getTime(); + var progress; + var easeProgress; // let el; + + if (!began) { + began = true; + if (params.begin) params.begin(els); + } + + if (startTime === null) { + startTime = time; + } + + if (params.progress) { + // eslint-disable-next-line + params.progress(els, Math.max(Math.min((time - startTime) / params.duration, 1), 0), startTime + params.duration - time < 0 ? 0 : startTime + params.duration - time, startTime); + } + + elements.forEach(function (element) { + var el = element; + if (done || el.done) return; + Object.keys(props).forEach(function (prop) { + if (done || el.done) return; + progress = Math.max(Math.min((time - startTime) / params.duration, 1), 0); + easeProgress = a.easingProgress(params.easing, progress); + var _el$prop = el[prop], + initialValue = _el$prop.initialValue, + finalValue = _el$prop.finalValue, + unit = _el$prop.unit; + el[prop].currentValue = initialValue + easeProgress * (finalValue - initialValue); + var currentValue = el[prop].currentValue; + + if (finalValue > initialValue && currentValue >= finalValue || finalValue < initialValue && currentValue <= finalValue) { + el.container.style[prop] = finalValue + unit; + propsDone += 1; + + if (propsDone === Object.keys(props).length) { + el.done = true; + elementsDone += 1; + } + + if (elementsDone === elements.length) { + done = true; + } + } + + if (done) { + a.done(params.complete); + return; + } + + el.container.style[prop] = currentValue + unit; + }); + }); + if (done) return; // Then call + + a.frameId = window.requestAnimationFrame(render); + } + + a.frameId = window.requestAnimationFrame(render); + return a; + } + }; + + if (a.elements.length === 0) { + return els; + } + + var animateInstance; + + for (var i = 0; i < a.elements.length; i += 1) { + if (a.elements[i].dom7AnimateInstance) { + animateInstance = a.elements[i].dom7AnimateInstance; + } else a.elements[i].dom7AnimateInstance = a; + } + + if (!animateInstance) { + animateInstance = a; + } + + if (initialProps === 'stop') { + animateInstance.stop(); + } else { + animateInstance.animate(a.props, a.params); + } + + return els; + } + + function stop() { + var els = this; + + for (var i = 0; i < els.length; i += 1) { + if (els[i].dom7AnimateInstance) { + els[i].dom7AnimateInstance.stop(); + } + } + } + + var noTrigger = 'resize scroll'.split(' '); + + function shortcut(name) { + function eventHandler() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + if (typeof args[0] === 'undefined') { + for (var i = 0; i < this.length; i += 1) { + if (noTrigger.indexOf(name) < 0) { + if (name in this[i]) this[i][name]();else { + $(this[i]).trigger(name); + } + } + } + + return this; + } + + return this.on.apply(this, [name].concat(args)); + } + + return eventHandler; + } + + var click = shortcut('click'); + var blur = shortcut('blur'); + var focus = shortcut('focus'); + var focusin = shortcut('focusin'); + var focusout = shortcut('focusout'); + var keyup = shortcut('keyup'); + var keydown = shortcut('keydown'); + var keypress = shortcut('keypress'); + var submit = shortcut('submit'); + var change = shortcut('change'); + var mousedown = shortcut('mousedown'); + var mousemove = shortcut('mousemove'); + var mouseup = shortcut('mouseup'); + var mouseenter = shortcut('mouseenter'); + var mouseleave = shortcut('mouseleave'); + var mouseout = shortcut('mouseout'); + var mouseover = shortcut('mouseover'); + var touchstart = shortcut('touchstart'); + var touchend = shortcut('touchend'); + var touchmove = shortcut('touchmove'); + var resize = shortcut('resize'); + var scroll = shortcut('scroll'); + + var $_1 = $; + var add_1 = add; + var addClass_1 = addClass; + var animate_1 = animate; + var animationEnd_1 = animationEnd; + var append_1 = append; + var appendTo_1 = appendTo; + var attr_1 = attr; + var blur_1 = blur; + var change_1 = change; + var children_1 = children; + var click_1 = click; + var closest_1 = closest; + var css_1 = css; + var data_1 = data; + var dataset_1 = dataset; + var _default$1 = $; + var detach_1 = detach; + var each_1 = each; + var empty_1 = empty; + var eq_1 = eq; + var filter_1 = filter; + var find_1 = find; + var focus_1 = focus; + var focusin_1 = focusin; + var focusout_1 = focusout; + var hasClass_1 = hasClass; + var height_1 = height; + var hide_1 = hide; + var html_1 = html; + var index_1 = index$1; + var insertAfter_1 = insertAfter; + var insertBefore_1 = insertBefore; + var is_1 = is; + var keydown_1 = keydown; + var keypress_1 = keypress; + var keyup_1 = keyup; + var mousedown_1 = mousedown; + var mouseenter_1 = mouseenter; + var mouseleave_1 = mouseleave; + var mousemove_1 = mousemove; + var mouseout_1 = mouseout; + var mouseover_1 = mouseover; + var mouseup_1 = mouseup; + var next_1 = next; + var nextAll_1 = nextAll; + var off_1 = off; + var offset_1 = offset; + var on_1 = on; + var once_1 = once; + var outerHeight_1 = outerHeight; + var outerWidth_1 = outerWidth; + var parent_1 = parent; + var parents_1 = parents; + var prepend_1 = prepend; + var prependTo_1 = prependTo; + var prev_1 = prev; + var prevAll_1 = prevAll; + var prop_1 = prop; + var remove_1 = remove; + var removeAttr_1 = removeAttr; + var removeClass_1 = removeClass; + var removeData_1 = removeData; + var resize_1 = resize; + var scroll_1 = scroll; + var scrollLeft_1 = scrollLeft; + var scrollTo_1 = scrollTo; + var scrollTop_1 = scrollTop; + var show_1 = show; + var siblings_1 = siblings; + var stop_1 = stop; + var styles_1 = styles; + var submit_1 = submit; + var text_1 = text; + var toggleClass_1 = toggleClass; + var touchend_1 = touchend; + var touchmove_1 = touchmove; + var touchstart_1 = touchstart; + var transform_1 = transform; + var transition_1 = transition; + var transitionEnd_1 = transitionEnd; + var trigger_1 = trigger; + var val_1 = val; + var value_1 = value; + var width_1 = width; + + var dom7_cjs = /*#__PURE__*/Object.defineProperty({ + $: $_1, + add: add_1, + addClass: addClass_1, + animate: animate_1, + animationEnd: animationEnd_1, + append: append_1, + appendTo: appendTo_1, + attr: attr_1, + blur: blur_1, + change: change_1, + children: children_1, + click: click_1, + closest: closest_1, + css: css_1, + data: data_1, + dataset: dataset_1, + default: _default$1, + detach: detach_1, + each: each_1, + empty: empty_1, + eq: eq_1, + filter: filter_1, + find: find_1, + focus: focus_1, + focusin: focusin_1, + focusout: focusout_1, + hasClass: hasClass_1, + height: height_1, + hide: hide_1, + html: html_1, + index: index_1, + insertAfter: insertAfter_1, + insertBefore: insertBefore_1, + is: is_1, + keydown: keydown_1, + keypress: keypress_1, + keyup: keyup_1, + mousedown: mousedown_1, + mouseenter: mouseenter_1, + mouseleave: mouseleave_1, + mousemove: mousemove_1, + mouseout: mouseout_1, + mouseover: mouseover_1, + mouseup: mouseup_1, + next: next_1, + nextAll: nextAll_1, + off: off_1, + offset: offset_1, + on: on_1, + once: once_1, + outerHeight: outerHeight_1, + outerWidth: outerWidth_1, + parent: parent_1, + parents: parents_1, + prepend: prepend_1, + prependTo: prependTo_1, + prev: prev_1, + prevAll: prevAll_1, + prop: prop_1, + remove: remove_1, + removeAttr: removeAttr_1, + removeClass: removeClass_1, + removeData: removeData_1, + resize: resize_1, + scroll: scroll_1, + scrollLeft: scrollLeft_1, + scrollTo: scrollTo_1, + scrollTop: scrollTop_1, + show: show_1, + siblings: siblings_1, + stop: stop_1, + styles: styles_1, + submit: submit_1, + text: text_1, + toggleClass: toggleClass_1, + touchend: touchend_1, + touchmove: touchmove_1, + touchstart: touchstart_1, + transform: transform_1, + transition: transition_1, + transitionEnd: transitionEnd_1, + trigger: trigger_1, + val: val_1, + value: value_1, + width: width_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]'; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** Used for built-in method references. */ + var objectProto$3 = Object.prototype; + + /** Used to check objects for own properties. */ + var hasOwnProperty$1 = objectProto$3.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$3 = objectProto$3.toString; + + /** Built-in value references. */ + var propertyIsEnumerable = objectProto$3.propertyIsEnumerable; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeKeys = overArg(Object.keys, Object); + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty$1.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.forEach` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEach = createBaseEach(baseForOwn); + + /** + * The base implementation of `baseForOwn` which iterates over `object` + * properties returned by `keysFunc` and invokes `iteratee` for each property. + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseFor = createBaseFor(); + + /** + * The base implementation of `_.forOwn` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwn(object, iteratee) { + return object && baseFor(object, iteratee, keys); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty$1.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + if (collection == null) { + return collection; + } + if (!isArrayLike(collection)) { + return eachFunc(collection, iteratee); + } + var length = collection.length, + index = fromRight ? length : -1, + iterable = Object(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for methods like `_.forIn` and `_.forOwn`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var index = -1, + iterable = Object(object), + props = keysFunc(object), + length = props.length; + + while (length--) { + var key = props[fromRight ? length : ++index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$3; + + return value === proto; + } + + /** + * Iterates over elements of `collection` and invokes `iteratee` for each element. + * The iteratee is invoked with three arguments: (value, index|key, collection). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * **Note:** As with other "Collections" methods, objects with a "length" + * property are iterated like arrays. To avoid this behavior use `_.forIn` + * or `_.forOwn` for object iteration. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias each + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEachRight + * @example + * + * _([1, 2]).forEach(function(value) { + * console.log(value); + * }); + * // => Logs `1` then `2`. + * + * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forEach(collection, iteratee) { + var func = isArray(collection) ? arrayEach : baseEach; + return func(collection, typeof iteratee == 'function' ? iteratee : identity); + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty$1.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString$3.call(value) == argsTag); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike$3(value) && isArrayLike(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject$2(value) ? objectToString$3.call(value) : ''; + return tag == funcTag || tag == genTag; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$2(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$3(value) { + return !!value && typeof value == 'object'; + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * This method returns the first argument it receives. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Util + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'a': 1 }; + * + * console.log(_.identity(object) === object); + * // => true + */ + function identity(value) { + return value; + } + + var lodash_foreach = forEach; + + let urlAlphabet$1 = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + var urlAlphabet_1 = { urlAlphabet: urlAlphabet$1 }; + + let { urlAlphabet } = urlAlphabet_1; + { + if ( + typeof navigator !== 'undefined' && + navigator.product === 'ReactNative' && + typeof crypto === 'undefined' + ) { + throw new Error( + 'React Native does not have a built-in secure random generator. ' + + 'If you don’t need unpredictable IDs use `nanoid/non-secure`. ' + + 'For secure IDs, import `react-native-get-random-values` ' + + 'before Nano ID.' + ) + } + if (typeof msCrypto !== 'undefined' && typeof crypto === 'undefined') { + throw new Error( + 'Import file with `if (!window.crypto) window.crypto = window.msCrypto`' + + ' before importing Nano ID to fix IE 11 support' + ) + } + if (typeof crypto === 'undefined') { + throw new Error( + 'Your browser does not have secure random generator. ' + + 'If you don’t need unpredictable IDs, you can use nanoid/non-secure.' + ) + } + } + let random = bytes => crypto.getRandomValues(new Uint8Array(bytes)); + let customRandom = (alphabet, size, getRandom) => { + let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1; + let step = -~((1.6 * mask * size) / alphabet.length); + return () => { + let id = ''; + while (true) { + let bytes = getRandom(step); + let j = step; + while (j--) { + id += alphabet[bytes[j] & mask] || ''; + if (id.length === size) return id + } + } + } + }; + let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random); + let nanoid$2 = (size = 21) => { + let id = ''; + let bytes = crypto.getRandomValues(new Uint8Array(size)); + while (size--) { + let byte = bytes[size] & 63; + if (byte < 36) { + id += byte.toString(36); + } else if (byte < 62) { + id += (byte - 26).toString(36).toUpperCase(); + } else if (byte < 63) { + id += '_'; + } else { + id += '-'; + } + } + return id + }; + var index_browser = { nanoid: nanoid$2, customAlphabet, customRandom, urlAlphabet, random }; + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT$1 = 'Expected a function'; + + /** Used as references for various `Number` constants. */ + var NAN$1 = 0 / 0; + + /** `Object#toString` result references. */ + var symbolTag$2 = '[object Symbol]'; + + /** Used to match leading and trailing whitespace. */ + var reTrim$1 = /^\s+|\s+$/g; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex$1 = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary$1 = /^0b[01]+$/i; + + /** Used to detect octal string values. */ + var reIsOctal$1 = /^0o[0-7]+$/i; + + /** Built-in method references without a dependency on `root`. */ + var freeParseInt$1 = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$2 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$2 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$2 = freeGlobal$2 || freeSelf$2 || Function('return this')(); + + /** Used for built-in method references. */ + var objectProto$2 = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$2 = objectProto$2.toString; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax$1 = Math.max, + nativeMin$1 = Math.min; + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now$1 = function() { + return root$2.Date.now(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce$2(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + wait = toNumber$1(wait) || 0; + if (isObject$1(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax$1(toNumber$1(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin$1(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now$1(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now$1()); + } + + function debounced() { + var time = now$1(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + if (isObject$1(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce$2(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$1(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$2(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol$2(value) { + return typeof value == 'symbol' || + (isObjectLike$2(value) && objectToString$2.call(value) == symbolTag$2); + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber$1(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol$2(value)) { + return NAN$1; + } + if (isObject$1(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject$1(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim$1, ''); + var isBinary = reIsBinary$1.test(value); + return (isBinary || reIsOctal$1.test(value)) + ? freeParseInt$1(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex$1.test(value) ? NAN$1 : +value); + } + + var lodash_throttle = throttle; + + var snabbdom_cjs = createCommonjsModule$1(function (module, exports) { + + Object.defineProperty(exports, '__esModule', { value: true }); + + function createElement(tagName, options) { + return document.createElement(tagName, options); + } + function createElementNS(namespaceURI, qualifiedName, options) { + return document.createElementNS(namespaceURI, qualifiedName, options); + } + function createTextNode(text) { + return document.createTextNode(text); + } + function createComment(text) { + return document.createComment(text); + } + function insertBefore(parentNode, newNode, referenceNode) { + parentNode.insertBefore(newNode, referenceNode); + } + function removeChild(node, child) { + node.removeChild(child); + } + function appendChild(node, child) { + node.appendChild(child); + } + function parentNode(node) { + return node.parentNode; + } + function nextSibling(node) { + return node.nextSibling; + } + function tagName(elm) { + return elm.tagName; + } + function setTextContent(node, text) { + node.textContent = text; + } + function getTextContent(node) { + return node.textContent; + } + function isElement(node) { + return node.nodeType === 1; + } + function isText(node) { + return node.nodeType === 3; + } + function isComment(node) { + return node.nodeType === 8; + } + const htmlDomApi = { + createElement, + createElementNS, + createTextNode, + createComment, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, + getTextContent, + isElement, + isText, + isComment, + }; + + function vnode(sel, data, children, text, elm) { + const key = data === undefined ? undefined : data.key; + return { sel, data, children, text, elm, key }; + } + + const array = Array.isArray; + function primitive(s) { + return typeof s === "string" || + typeof s === "number" || + s instanceof String || + s instanceof Number; + } + + function isUndef(s) { + return s === undefined; + } + function isDef(s) { + return s !== undefined; + } + const emptyNode = vnode("", {}, [], undefined, undefined); + function sameVnode(vnode1, vnode2) { + var _a, _b; + const isSameKey = vnode1.key === vnode2.key; + const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is); + const isSameSel = vnode1.sel === vnode2.sel; + return isSameSel && isSameKey && isSameIs; + } + function isVnode(vnode) { + return vnode.sel !== undefined; + } + function createKeyToOldIdx(children, beginIdx, endIdx) { + var _a; + const map = {}; + for (let i = beginIdx; i <= endIdx; ++i) { + const key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key; + if (key !== undefined) { + map[key] = i; + } + } + return map; + } + const hooks = [ + "create", + "update", + "remove", + "destroy", + "pre", + "post", + ]; + function init$1(modules, domApi) { + const cbs = { + create: [], + update: [], + remove: [], + destroy: [], + pre: [], + post: [], + }; + const api = domApi !== undefined ? domApi : htmlDomApi; + for (const hook of hooks) { + for (const module of modules) { + const currentHook = module[hook]; + if (currentHook !== undefined) { + cbs[hook].push(currentHook); + } + } + } + function emptyNodeAt(elm) { + const id = elm.id ? "#" + elm.id : ""; + // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot. + // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring + const classes = elm.getAttribute("class"); + const c = classes ? "." + classes.split(" ").join(".") : ""; + return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); + } + function createRmCb(childElm, listeners) { + return function rmCb() { + if (--listeners === 0) { + const parent = api.parentNode(childElm); + api.removeChild(parent, childElm); + } + }; + } + function createElm(vnode, insertedVnodeQueue) { + var _a, _b; + let i; + let data = vnode.data; + if (data !== undefined) { + const init = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init; + if (isDef(init)) { + init(vnode); + data = vnode.data; + } + } + const children = vnode.children; + const sel = vnode.sel; + if (sel === "!") { + if (isUndef(vnode.text)) { + vnode.text = ""; + } + vnode.elm = api.createComment(vnode.text); + } + else if (sel !== undefined) { + // Parse selector + const hashIdx = sel.indexOf("#"); + const dotIdx = sel.indexOf(".", hashIdx); + const hash = hashIdx > 0 ? hashIdx : sel.length; + const dot = dotIdx > 0 ? dotIdx : sel.length; + const tag = hashIdx !== -1 || dotIdx !== -1 + ? sel.slice(0, Math.min(hash, dot)) + : sel; + const elm = (vnode.elm = + isDef(data) && isDef((i = data.ns)) + ? api.createElementNS(i, tag, data) + : api.createElement(tag, data)); + if (hash < dot) + elm.setAttribute("id", sel.slice(hash + 1, dot)); + if (dotIdx > 0) + elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); + for (i = 0; i < cbs.create.length; ++i) + cbs.create[i](emptyNode, vnode); + if (array(children)) { + for (i = 0; i < children.length; ++i) { + const ch = children[i]; + if (ch != null) { + api.appendChild(elm, createElm(ch, insertedVnodeQueue)); + } + } + } + else if (primitive(vnode.text)) { + api.appendChild(elm, api.createTextNode(vnode.text)); + } + const hook = vnode.data.hook; + if (isDef(hook)) { + (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode); + if (hook.insert) { + insertedVnodeQueue.push(vnode); + } + } + } + else { + vnode.elm = api.createTextNode(vnode.text); + } + return vnode.elm; + } + function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + const ch = vnodes[startIdx]; + if (ch != null) { + api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); + } + } + } + function invokeDestroyHook(vnode) { + var _a, _b; + const data = vnode.data; + if (data !== undefined) { + (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); + for (let i = 0; i < cbs.destroy.length; ++i) + cbs.destroy[i](vnode); + if (vnode.children !== undefined) { + for (let j = 0; j < vnode.children.length; ++j) { + const child = vnode.children[j]; + if (child != null && typeof child !== "string") { + invokeDestroyHook(child); + } + } + } + } + } + function removeVnodes(parentElm, vnodes, startIdx, endIdx) { + var _a, _b; + for (; startIdx <= endIdx; ++startIdx) { + let listeners; + let rm; + const ch = vnodes[startIdx]; + if (ch != null) { + if (isDef(ch.sel)) { + invokeDestroyHook(ch); + listeners = cbs.remove.length + 1; + rm = createRmCb(ch.elm, listeners); + for (let i = 0; i < cbs.remove.length; ++i) + cbs.remove[i](ch, rm); + const removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove; + if (isDef(removeHook)) { + removeHook(ch, rm); + } + else { + rm(); + } + } + else { + // Text node + api.removeChild(parentElm, ch.elm); + } + } + } + } + function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { + let oldStartIdx = 0; + let newStartIdx = 0; + let oldEndIdx = oldCh.length - 1; + let oldStartVnode = oldCh[0]; + let oldEndVnode = oldCh[oldEndIdx]; + let newEndIdx = newCh.length - 1; + let newStartVnode = newCh[0]; + let newEndVnode = newCh[newEndIdx]; + let oldKeyToIdx; + let idxInOld; + let elmToMove; + let before; + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (oldStartVnode == null) { + oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left + } + else if (oldEndVnode == null) { + oldEndVnode = oldCh[--oldEndIdx]; + } + else if (newStartVnode == null) { + newStartVnode = newCh[++newStartIdx]; + } + else if (newEndVnode == null) { + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } + else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldStartVnode, newEndVnode)) { + // Vnode moved right + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldEndVnode, newStartVnode)) { + // Vnode moved left + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } + else { + if (oldKeyToIdx === undefined) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = oldKeyToIdx[newStartVnode.key]; + if (isUndef(idxInOld)) { + // New element + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } + else { + elmToMove = oldCh[idxInOld]; + if (elmToMove.sel !== newStartVnode.sel) { + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } + else { + patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); + oldCh[idxInOld] = undefined; + api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { + if (oldStartIdx > oldEndIdx) { + before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } + else { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + } + function patchVnode(oldVnode, vnode, insertedVnodeQueue) { + var _a, _b, _c, _d, _e; + const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook; + (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); + const elm = (vnode.elm = oldVnode.elm); + const oldCh = oldVnode.children; + const ch = vnode.children; + if (oldVnode === vnode) + return; + if (vnode.data !== undefined) { + for (let i = 0; i < cbs.update.length; ++i) + cbs.update[i](oldVnode, vnode); + (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode); + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) + updateChildren(elm, oldCh, ch, insertedVnodeQueue); + } + else if (isDef(ch)) { + if (isDef(oldVnode.text)) + api.setTextContent(elm, ""); + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } + else if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } + else if (isDef(oldVnode.text)) { + api.setTextContent(elm, ""); + } + } + else if (oldVnode.text !== vnode.text) { + if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } + api.setTextContent(elm, vnode.text); + } + (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); + } + return function patch(oldVnode, vnode) { + let i, elm, parent; + const insertedVnodeQueue = []; + for (i = 0; i < cbs.pre.length; ++i) + cbs.pre[i](); + if (!isVnode(oldVnode)) { + oldVnode = emptyNodeAt(oldVnode); + } + if (sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue); + } + else { + elm = oldVnode.elm; + parent = api.parentNode(elm); + createElm(vnode, insertedVnodeQueue); + if (parent !== null) { + api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); + removeVnodes(parent, [oldVnode], 0, 0); + } + } + for (i = 0; i < insertedVnodeQueue.length; ++i) { + insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); + } + for (i = 0; i < cbs.post.length; ++i) + cbs.post[i](); + return vnode; + }; + } + + function addNS(data, children, sel) { + data.ns = "http://www.w3.org/2000/svg"; + if (sel !== "foreignObject" && children !== undefined) { + for (let i = 0; i < children.length; ++i) { + const childData = children[i].data; + if (childData !== undefined) { + addNS(childData, children[i].children, children[i].sel); + } + } + } + } + function h(sel, b, c) { + let data = {}; + let children; + let text; + let i; + if (c !== undefined) { + if (b !== null) { + data = b; + } + if (array(c)) { + children = c; + } + else if (primitive(c)) { + text = c.toString(); + } + else if (c && c.sel) { + children = [c]; + } + } + else if (b !== undefined && b !== null) { + if (array(b)) { + children = b; + } + else if (primitive(b)) { + text = b.toString(); + } + else if (b && b.sel) { + children = [b]; + } + else { + data = b; + } + } + if (children !== undefined) { + for (i = 0; i < children.length; ++i) { + if (primitive(children[i])) + children[i] = vnode(undefined, undefined, undefined, children[i], undefined); + } + } + if (sel[0] === "s" && + sel[1] === "v" && + sel[2] === "g" && + (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { + addNS(data, children, sel); + } + return vnode(sel, data, children, text, undefined); + } + + function copyToThunk(vnode, thunk) { + vnode.data.fn = thunk.data.fn; + vnode.data.args = thunk.data.args; + thunk.data = vnode.data; + thunk.children = vnode.children; + thunk.text = vnode.text; + thunk.elm = vnode.elm; + } + function init(thunk) { + const cur = thunk.data; + const vnode = cur.fn(...cur.args); + copyToThunk(vnode, thunk); + } + function prepatch(oldVnode, thunk) { + let i; + const old = oldVnode.data; + const cur = thunk.data; + const oldArgs = old.args; + const args = cur.args; + if (old.fn !== cur.fn || oldArgs.length !== args.length) { + copyToThunk(cur.fn(...args), thunk); + return; + } + for (i = 0; i < args.length; ++i) { + if (oldArgs[i] !== args[i]) { + copyToThunk(cur.fn(...args), thunk); + return; + } + } + copyToThunk(oldVnode, thunk); + } + const thunk = function thunk(sel, key, fn, args) { + if (args === undefined) { + args = fn; + fn = key; + key = undefined; + } + return h(sel, { + key: key, + hook: { init, prepatch }, + fn: fn, + args: args, + }); + }; + + function pre(vnode, newVnode) { + const attachData = vnode.data.attachData; + // Copy created placeholder and real element from old vnode + newVnode.data.attachData.placeholder = attachData.placeholder; + newVnode.data.attachData.real = attachData.real; + // Mount real element in vnode so the patch process operates on it + vnode.elm = vnode.data.attachData.real; + } + function post(_, vnode) { + // Mount dummy placeholder in vnode so potential reorders use it + vnode.elm = vnode.data.attachData.placeholder; + } + function destroy(vnode) { + // Remove placeholder + if (vnode.elm !== undefined) { + vnode.elm.parentNode.removeChild(vnode.elm); + } + // Remove real element from where it was inserted + vnode.elm = vnode.data.attachData.real; + } + function create(_, vnode) { + const real = vnode.elm; + const attachData = vnode.data.attachData; + const placeholder = document.createElement("span"); + // Replace actual element with dummy placeholder + // Snabbdom will then insert placeholder instead + vnode.elm = placeholder; + attachData.target.appendChild(real); + attachData.real = real; + attachData.placeholder = placeholder; + } + function attachTo(target, vnode) { + if (vnode.data === undefined) + vnode.data = {}; + if (vnode.data.hook === undefined) + vnode.data.hook = {}; + const data = vnode.data; + const hook = vnode.data.hook; + data.attachData = { target: target, placeholder: undefined, real: undefined }; + hook.create = create; + hook.prepatch = pre; + hook.postpatch = post; + hook.destroy = destroy; + return vnode; + } + + function toVNode(node, domApi) { + const api = domApi !== undefined ? domApi : htmlDomApi; + let text; + if (api.isElement(node)) { + const id = node.id ? "#" + node.id : ""; + const cn = node.getAttribute("class"); + const c = cn ? "." + cn.split(" ").join(".") : ""; + const sel = api.tagName(node).toLowerCase() + id + c; + const attrs = {}; + const children = []; + let name; + let i, n; + const elmAttrs = node.attributes; + const elmChildren = node.childNodes; + for (i = 0, n = elmAttrs.length; i < n; i++) { + name = elmAttrs[i].nodeName; + if (name !== "id" && name !== "class") { + attrs[name] = elmAttrs[i].nodeValue; + } + } + for (i = 0, n = elmChildren.length; i < n; i++) { + children.push(toVNode(elmChildren[i], domApi)); + } + return vnode(sel, { attrs }, children, undefined, node); + } + else if (api.isText(node)) { + text = api.getTextContent(node); + return vnode(undefined, undefined, undefined, text, node); + } + else if (api.isComment(node)) { + text = api.getTextContent(node); + return vnode("!", {}, [], text, node); + } + else { + return vnode("", {}, [], undefined, node); + } + } + + const xlinkNS = "http://www.w3.org/1999/xlink"; + const xmlNS = "http://www.w3.org/XML/1998/namespace"; + const colonChar = 58; + const xChar = 120; + function updateAttrs(oldVnode, vnode) { + let key; + const elm = vnode.elm; + let oldAttrs = oldVnode.data.attrs; + let attrs = vnode.data.attrs; + if (!oldAttrs && !attrs) + return; + if (oldAttrs === attrs) + return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + // update modified attributes, add new attributes + for (key in attrs) { + const cur = attrs[key]; + const old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ""); + } + else if (cur === false) { + elm.removeAttribute(key); + } + else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } + else if (key.charCodeAt(3) === colonChar) { + // Assume xml namespace + elm.setAttributeNS(xmlNS, key, cur); + } + else if (key.charCodeAt(5) === colonChar) { + // Assume xlink namespace + elm.setAttributeNS(xlinkNS, key, cur); + } + else { + elm.setAttribute(key, cur); + } + } + } + } + // remove removed attributes + // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) + // the other option is to remove all attributes with value == undefined + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } + } + const attributesModule = { + create: updateAttrs, + update: updateAttrs, + }; + + function updateClass(oldVnode, vnode) { + let cur; + let name; + const elm = vnode.elm; + let oldClass = oldVnode.data.class; + let klass = vnode.data.class; + if (!oldClass && !klass) + return; + if (oldClass === klass) + return; + oldClass = oldClass || {}; + klass = klass || {}; + for (name in oldClass) { + if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { + // was `true` and now not provided + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + elm.classList[cur ? "add" : "remove"](name); + } + } + } + const classModule = { create: updateClass, update: updateClass }; + + const CAPS_REGEX = /[A-Z]/g; + function updateDataset(oldVnode, vnode) { + const elm = vnode.elm; + let oldDataset = oldVnode.data.dataset; + let dataset = vnode.data.dataset; + let key; + if (!oldDataset && !dataset) + return; + if (oldDataset === dataset) + return; + oldDataset = oldDataset || {}; + dataset = dataset || {}; + const d = elm.dataset; + for (key in oldDataset) { + if (!dataset[key]) { + if (d) { + if (key in d) { + delete d[key]; + } + } + else { + elm.removeAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase()); + } + } + } + for (key in dataset) { + if (oldDataset[key] !== dataset[key]) { + if (d) { + d[key] = dataset[key]; + } + else { + elm.setAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase(), dataset[key]); + } + } + } + } + const datasetModule = { + create: updateDataset, + update: updateDataset, + }; + + function invokeHandler(handler, vnode, event) { + if (typeof handler === "function") { + // call function handler + handler.call(vnode, event, vnode); + } + else if (typeof handler === "object") { + // call multiple handlers + for (let i = 0; i < handler.length; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } + function handleEvent(event, vnode) { + const name = event.type; + const on = vnode.data.on; + // call event handler(s) if exists + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } + } + function createListener() { + return function handler(event) { + handleEvent(event, handler.vnode); + }; + } + function updateEventListeners(oldVnode, vnode) { + const oldOn = oldVnode.data.on; + const oldListener = oldVnode.listener; + const oldElm = oldVnode.elm; + const on = vnode && vnode.data.on; + const elm = (vnode && vnode.elm); + let name; + // optimization for reused immutable handlers + if (oldOn === on) { + return; + } + // remove existing listeners which no longer used + if (oldOn && oldListener) { + // if element changed or deleted we remove all existing listeners unconditionally + if (!on) { + for (name in oldOn) { + // remove listener if element was changed or existing listeners removed + oldElm.removeEventListener(name, oldListener, false); + } + } + else { + for (name in oldOn) { + // remove listener if existing listener removed + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + // add new listeners which has not already attached + if (on) { + // reuse existing listener or create new + const listener = (vnode.listener = + oldVnode.listener || createListener()); + // update vnode for listener + listener.vnode = vnode; + // if element changed or added we add all needed listeners unconditionally + if (!oldOn) { + for (name in on) { + // add listener if element was changed or new listeners added + elm.addEventListener(name, listener, false); + } + } + else { + for (name in on) { + // add listener if new listener added + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } + } + const eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners, + }; + + function updateProps(oldVnode, vnode) { + let key; + let cur; + let old; + const elm = vnode.elm; + let oldProps = oldVnode.data.props; + let props = vnode.data.props; + if (!oldProps && !props) + return; + if (oldProps === props) + return; + oldProps = oldProps || {}; + props = props || {}; + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== "value" || elm[key] !== cur)) { + elm[key] = cur; + } + } + } + const propsModule = { create: updateProps, update: updateProps }; + + // Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409. + const raf = (typeof window !== "undefined" && + window.requestAnimationFrame.bind(window)) || + setTimeout; + const nextFrame = function (fn) { + raf(function () { + raf(fn); + }); + }; + let reflowForced = false; + function setNextFrame(obj, prop, val) { + nextFrame(function () { + obj[prop] = val; + }); + } + function updateStyle(oldVnode, vnode) { + let cur; + let name; + const elm = vnode.elm; + let oldStyle = oldVnode.data.style; + let style = vnode.data.style; + if (!oldStyle && !style) + return; + if (oldStyle === style) + return; + oldStyle = oldStyle || {}; + style = style || {}; + const oldHasDel = "delayed" in oldStyle; + for (name in oldStyle) { + if (!style[name]) { + if (name[0] === "-" && name[1] === "-") { + elm.style.removeProperty(name); + } + else { + elm.style[name] = ""; + } + } + } + for (name in style) { + cur = style[name]; + if (name === "delayed" && style.delayed) { + for (const name2 in style.delayed) { + cur = style.delayed[name2]; + if (!oldHasDel || cur !== oldStyle.delayed[name2]) { + setNextFrame(elm.style, name2, cur); + } + } + } + else if (name !== "remove" && cur !== oldStyle[name]) { + if (name[0] === "-" && name[1] === "-") { + elm.style.setProperty(name, cur); + } + else { + elm.style[name] = cur; + } + } + } + } + function applyDestroyStyle(vnode) { + let style; + let name; + const elm = vnode.elm; + const s = vnode.data.style; + if (!s || !(style = s.destroy)) + return; + for (name in style) { + elm.style[name] = style[name]; + } + } + function applyRemoveStyle(vnode, rm) { + const s = vnode.data.style; + if (!s || !s.remove) { + rm(); + return; + } + if (!reflowForced) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + vnode.elm.offsetLeft; + reflowForced = true; + } + let name; + const elm = vnode.elm; + let i = 0; + const style = s.remove; + let amount = 0; + const applied = []; + for (name in style) { + applied.push(name); + elm.style[name] = style[name]; + } + const compStyle = getComputedStyle(elm); + const props = compStyle["transition-property"].split(", "); + for (; i < props.length; ++i) { + if (applied.indexOf(props[i]) !== -1) + amount++; + } + elm.addEventListener("transitionend", function (ev) { + if (ev.target === elm) + --amount; + if (amount === 0) + rm(); + }); + } + function forceReflow() { + reflowForced = false; + } + const styleModule = { + pre: forceReflow, + create: updateStyle, + update: updateStyle, + destroy: applyDestroyStyle, + remove: applyRemoveStyle, + }; + + /* eslint-disable @typescript-eslint/no-namespace, import/export */ + function flattenAndFilter(children, flattened) { + for (const child of children) { + // filter out falsey children, except 0 since zero can be a valid value e.g inside a chart + if (child !== undefined && + child !== null && + child !== false && + child !== "") { + if (Array.isArray(child)) { + flattenAndFilter(child, flattened); + } + else if (typeof child === "string" || + typeof child === "number" || + typeof child === "boolean") { + flattened.push(vnode(undefined, undefined, undefined, String(child), undefined)); + } + else { + flattened.push(child); + } + } + } + return flattened; + } + /** + * jsx/tsx compatible factory function + * see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions + */ + function jsx(tag, data, ...children) { + const flatChildren = flattenAndFilter(children, []); + if (typeof tag === "function") { + // tag is a function component + return tag(data, flatChildren); + } + else { + if (flatChildren.length === 1 && + !flatChildren[0].sel && + flatChildren[0].text) { + // only child is a simple text node, pass as text for a simpler vtree + return h(tag, data, flatChildren[0].text); + } + else { + return h(tag, data, flatChildren); + } + } + } + (function (jsx) { + })(jsx || (jsx = {})); + + exports.array = array; + exports.attachTo = attachTo; + exports.attributesModule = attributesModule; + exports.classModule = classModule; + exports.datasetModule = datasetModule; + exports.eventListenersModule = eventListenersModule; + exports.h = h; + exports.htmlDomApi = htmlDomApi; + exports.init = init$1; + exports.jsx = jsx; + exports.primitive = primitive; + exports.propsModule = propsModule; + exports.styleModule = styleModule; + exports.thunk = thunk; + exports.toVNode = toVNode; + exports.vnode = vnode; + }); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as references for various `Number` constants. */ + var INFINITY = 1 / 0; + + /** `Object#toString` result references. */ + var symbolTag$1 = '[object Symbol]'; + + /** Used to match words composed of alphanumeric characters. */ + var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + + /** Used to match Latin Unicode letters (excluding mathematical operators). */ + var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; + + /** Used to compose unicode character classes. */ + var rsAstralRange = '\\ud800-\\udfff', + rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23', + rsComboSymbolsRange = '\\u20d0-\\u20f0', + rsDingbatRange = '\\u2700-\\u27bf', + rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', + rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', + rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', + rsPunctuationRange = '\\u2000-\\u206f', + rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', + rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + rsVarRange = '\\ufe0e\\ufe0f', + rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; + + /** Used to compose unicode capture groups. */ + var rsApos = "['\u2019]", + rsAstral = '[' + rsAstralRange + ']', + rsBreak = '[' + rsBreakRange + ']', + rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']', + rsDigits = '\\d+', + rsDingbat = '[' + rsDingbatRange + ']', + rsLower = '[' + rsLowerRange + ']', + rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', + rsFitz = '\\ud83c[\\udffb-\\udfff]', + rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', + rsNonAstral = '[^' + rsAstralRange + ']', + rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsUpper = '[' + rsUpperRange + ']', + rsZWJ = '\\u200d'; + + /** Used to compose unicode regexes. */ + var rsLowerMisc = '(?:' + rsLower + '|' + rsMisc + ')', + rsUpperMisc = '(?:' + rsUpper + '|' + rsMisc + ')', + rsOptLowerContr = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', + rsOptUpperContr = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', + reOptMod = rsModifier + '?', + rsOptVar = '[' + rsVarRange + ']?', + rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', + rsSeq = rsOptVar + reOptMod + rsOptJoin, + rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, + rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; + + /** Used to match apostrophes. */ + var reApos = RegExp(rsApos, 'g'); + + /** + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and + * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). + */ + var reComboMark = RegExp(rsCombo, 'g'); + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); + + /** Used to match complex or compound words. */ + var reUnicodeWord = RegExp([ + rsUpper + '?' + rsLower + '+' + rsOptLowerContr + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', + rsUpperMisc + '+' + rsOptUpperContr + '(?=' + [rsBreak, rsUpper + rsLowerMisc, '$'].join('|') + ')', + rsUpper + '?' + rsLowerMisc + '+' + rsOptLowerContr, + rsUpper + '+' + rsOptUpperContr, + rsDigits, + rsEmoji + ].join('|'), 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboMarksRange + rsComboSymbolsRange + rsVarRange + ']'); + + /** Used to detect strings that need a more robust regexp to match words. */ + var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; + + /** Used to map Latin Unicode letters to basic Latin letters. */ + var deburredLetters = { + // Latin-1 Supplement block. + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss', + // Latin Extended-A block. + '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', + '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', + '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', + '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', + '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', + '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', + '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', + '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', + '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', + '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', + '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', + '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', + '\u0134': 'J', '\u0135': 'j', + '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', + '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', + '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', + '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', + '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', + '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', + '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', + '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', + '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', + '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', + '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', + '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', + '\u0163': 't', '\u0165': 't', '\u0167': 't', + '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', + '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', + '\u0174': 'W', '\u0175': 'w', + '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', + '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', + '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', + '\u0132': 'IJ', '\u0133': 'ij', + '\u0152': 'Oe', '\u0153': 'oe', + '\u0149': "'n", '\u017f': 'ss' + }; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$1 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$1 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$1 = freeGlobal$1 || freeSelf$1 || Function('return this')(); + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray(string) { + return string.split(''); + } + + /** + * Splits an ASCII `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function asciiWords(string) { + return string.match(reAsciiWord) || []; + } + + /** + * The base implementation of `_.propertyOf` without support for deep paths. + * + * @private + * @param {Object} object The object to query. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyOf(object) { + return function(key) { + return object == null ? undefined : object[key]; + }; + } + + /** + * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A + * letters to basic Latin letters. + * + * @private + * @param {string} letter The matched letter to deburr. + * @returns {string} Returns the deburred letter. + */ + var deburrLetter = basePropertyOf(deburredLetters); + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode(string) { + return reHasUnicode.test(string); + } + + /** + * Checks if `string` contains a word composed of Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a word is found, else `false`. + */ + function hasUnicodeWord(string) { + return reHasUnicodeWord.test(string); + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray(string) { + return hasUnicode(string) + ? unicodeToArray(string) + : asciiToArray(string); + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray(string) { + return string.match(reUnicode) || []; + } + + /** + * Splits a Unicode `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function unicodeWords(string) { + return string.match(reUnicodeWord) || []; + } + + /** Used for built-in method references. */ + var objectProto$1 = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$1 = objectProto$1.toString; + + /** Built-in value references. */ + var Symbol$1 = root$1.Symbol; + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol$1 ? Symbol$1.prototype : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + + /** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; + } + + /** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ + function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isSymbol$1(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * Casts `array` to a slice if it's needed. + * + * @private + * @param {Array} array The array to inspect. + * @param {number} start The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the cast slice. + */ + function castSlice(array, start, end) { + var length = array.length; + end = end === undefined ? length : end; + return (!start && end >= length) ? array : baseSlice(array, start, end); + } + + /** + * Creates a function like `_.lowerFirst`. + * + * @private + * @param {string} methodName The name of the `String` case method to use. + * @returns {Function} Returns the new case function. + */ + function createCaseFirst(methodName) { + return function(string) { + string = toString(string); + + var strSymbols = hasUnicode(string) + ? stringToArray(string) + : undefined; + + var chr = strSymbols + ? strSymbols[0] + : string.charAt(0); + + var trailing = strSymbols + ? castSlice(strSymbols, 1).join('') + : string.slice(1); + + return chr[methodName]() + trailing; + }; + } + + /** + * Creates a function like `_.camelCase`. + * + * @private + * @param {Function} callback The function to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); + }; + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$1(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol$1(value) { + return typeof value == 'symbol' || + (isObjectLike$1(value) && objectToString$1.call(value) == symbolTag$1); + } + + /** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {string} Returns the string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ + function toString(value) { + return value == null ? '' : baseToString(value); + } + + /** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the camel cased string. + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar--'); + * // => 'fooBar' + * + * _.camelCase('__FOO_BAR__'); + * // => 'fooBar' + */ + var camelCase = createCompounder(function(result, word, index) { + word = word.toLowerCase(); + return result + (index ? capitalize(word) : word); + }); + + /** + * Converts the first character of `string` to upper case and the remaining + * to lower case. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to capitalize. + * @returns {string} Returns the capitalized string. + * @example + * + * _.capitalize('FRED'); + * // => 'Fred' + */ + function capitalize(string) { + return upperFirst(toString(string).toLowerCase()); + } + + /** + * Deburrs `string` by converting + * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) + * letters to basic Latin letters and removing + * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the deburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = toString(string); + return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); + } + + /** + * Converts the first character of `string` to upper case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.upperFirst('fred'); + * // => 'Fred' + * + * _.upperFirst('FRED'); + * // => 'FRED' + */ + var upperFirst = createCaseFirst('toUpperCase'); + + /** + * Splits `string` into an array of its words. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {RegExp|string} [pattern] The pattern to match words. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the words of `string`. + * @example + * + * _.words('fred, barney, & pebbles'); + * // => ['fred', 'barney', 'pebbles'] + * + * _.words('fred, barney, & pebbles', /[^, ]+/g); + * // => ['fred', 'barney', '&', 'pebbles'] + */ + function words(string, pattern, guard) { + string = toString(string); + pattern = guard ? undefined : pattern; + + if (pattern === undefined) { + return hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string); + } + return string.match(pattern) || []; + } + + var lodash_camelcase = camelCase; + + /** + * Constants. + */ + + var IS_MAC = typeof window != 'undefined' && /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); + + var MODIFIERS = { + alt: 'altKey', + control: 'ctrlKey', + meta: 'metaKey', + shift: 'shiftKey' + }; + + var ALIASES = { + add: '+', + break: 'pause', + cmd: 'meta', + command: 'meta', + ctl: 'control', + ctrl: 'control', + del: 'delete', + down: 'arrowdown', + esc: 'escape', + ins: 'insert', + left: 'arrowleft', + mod: IS_MAC ? 'meta' : 'control', + opt: 'alt', + option: 'alt', + return: 'enter', + right: 'arrowright', + space: ' ', + spacebar: ' ', + up: 'arrowup', + win: 'meta', + windows: 'meta' + }; + + var CODES = { + backspace: 8, + tab: 9, + enter: 13, + shift: 16, + control: 17, + alt: 18, + pause: 19, + capslock: 20, + escape: 27, + ' ': 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + arrowleft: 37, + arrowup: 38, + arrowright: 39, + arrowdown: 40, + insert: 45, + delete: 46, + meta: 91, + numlock: 144, + scrolllock: 145, + ';': 186, + '=': 187, + ',': 188, + '-': 189, + '.': 190, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + '\'': 222 + }; + + for (var f = 1; f < 20; f++) { + CODES['f' + f] = 111 + f; + } + + /** + * Is hotkey? + */ + + function isHotkey(hotkey, options, event) { + if (options && !('byKey' in options)) { + event = options; + options = null; + } + + if (!Array.isArray(hotkey)) { + hotkey = [hotkey]; + } + + var array = hotkey.map(function (string) { + return parseHotkey(string, options); + }); + var check = function check(e) { + return array.some(function (object) { + return compareHotkey(object, e); + }); + }; + var ret = event == null ? check : check(event); + return ret; + } + + function isCodeHotkey(hotkey, event) { + return isHotkey(hotkey, event); + } + + function isKeyHotkey(hotkey, event) { + return isHotkey(hotkey, { byKey: true }, event); + } + + /** + * Parse. + */ + + function parseHotkey(hotkey, options) { + var byKey = options && options.byKey; + var ret = {}; + + // Special case to handle the `+` key since we use it as a separator. + hotkey = hotkey.replace('++', '+add'); + var values = hotkey.split('+'); + var length = values.length; + + // Ensure that all the modifiers are set to false unless the hotkey has them. + + for (var k in MODIFIERS) { + ret[MODIFIERS[k]] = false; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = values[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var value = _step.value; + + var optional = value.endsWith('?') && value.length > 1; + + if (optional) { + value = value.slice(0, -1); + } + + var name = toKeyName(value); + var modifier = MODIFIERS[name]; + + if (value.length > 1 && !modifier && !ALIASES[value] && !CODES[name]) { + throw new TypeError('Unknown modifier: "' + value + '"'); + } + + if (length === 1 || !modifier) { + if (byKey) { + ret.key = name; + } else { + ret.which = toKeyCode(value); + } + } + + if (modifier) { + ret[modifier] = optional ? null : true; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return ret; + } + + /** + * Compare. + */ + + function compareHotkey(object, event) { + for (var key in object) { + var expected = object[key]; + var actual = void 0; + + if (expected == null) { + continue; + } + + if (key === 'key' && event.key != null) { + actual = event.key.toLowerCase(); + } else if (key === 'which') { + actual = expected === 91 && event.which === 93 ? 91 : event.which; + } else { + actual = event[key]; + } + + if (actual == null && expected === false) { + continue; + } + + if (actual !== expected) { + return false; + } + } + + return true; + } + + /** + * Utils. + */ + + function toKeyCode(name) { + name = toKeyName(name); + var code = CODES[name] || name.toUpperCase().charCodeAt(0); + return code; + } + + function toKeyName(name) { + name = name.toLowerCase(); + name = ALIASES[name] || name; + return name; + } + + /** + * Export. + */ + + var _default = isHotkey; + var isHotkey_1 = isHotkey; + var isCodeHotkey_1 = isCodeHotkey; + var isKeyHotkey_1 = isKeyHotkey; + var parseHotkey_1 = parseHotkey; + var compareHotkey_1 = compareHotkey; + var toKeyCode_1 = toKeyCode; + var toKeyName_1 = toKeyName; + + var lib$4 = /*#__PURE__*/Object.defineProperty({ + default: _default, + isHotkey: isHotkey_1, + isCodeHotkey: isCodeHotkey_1, + isKeyHotkey: isKeyHotkey_1, + parseHotkey: parseHotkey_1, + compareHotkey: compareHotkey_1, + toKeyCode: toKeyCode_1, + toKeyName: toKeyName_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used as references for various `Number` constants. */ + var NAN = 0 / 0; + + /** `Object#toString` result references. */ + var symbolTag = '[object Symbol]'; + + /** Used to match leading and trailing whitespace. */ + var reTrim = /^\s+|\s+$/g; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + + /** Built-in method references without a dependency on `root`. */ + var freeParseInt = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Used for built-in method references. */ + var objectProto = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString = objectProto.toString; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, + nativeMin = Math.min; + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = function() { + return root.Date.now(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce$1(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && objectToString.call(value) == symbolTag); + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); + } + + var lodash_debounce = debounce$1; + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + var lodash_clonedeep = createCommonjsModule$1(function (module, exports) { + /** Used as the size to enable large array optimizations. */ + var LARGE_ARRAY_SIZE = 200; + + /** Used to stand-in for `undefined` hash values. */ + var HASH_UNDEFINED = '__lodash_hash_undefined__'; + + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + weakMapTag = '[object WeakMap]'; + + var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + + /** Used to match `RegExp` flags from their coerced string values. */ + var reFlags = /\w*$/; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** Used to identify `toStringTag` values supported by `_.clone`. */ + var cloneableTags = {}; + cloneableTags[argsTag] = cloneableTags[arrayTag] = + cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = + cloneableTags[boolTag] = cloneableTags[dateTag] = + cloneableTags[float32Tag] = cloneableTags[float64Tag] = + cloneableTags[int8Tag] = cloneableTags[int16Tag] = + cloneableTags[int32Tag] = cloneableTags[mapTag] = + cloneableTags[numberTag] = cloneableTags[objectTag] = + cloneableTags[regexpTag] = cloneableTags[setTag] = + cloneableTags[stringTag] = cloneableTags[symbolTag] = + cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = + cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; + cloneableTags[errorTag] = cloneableTags[funcTag] = + cloneableTags[weakMapTag] = false; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Detect free variable `exports`. */ + var freeExports = exports && !exports.nodeType && exports; + + /** Detect free variable `module`. */ + var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports`. */ + var moduleExports = freeModule && freeModule.exports === freeExports; + + /** + * Adds the key-value `pair` to `map`. + * + * @private + * @param {Object} map The map to modify. + * @param {Array} pair The key-value pair to add. + * @returns {Object} Returns `map`. + */ + function addMapEntry(map, pair) { + // Don't return `map.set` because it's not chainable in IE 11. + map.set(pair[0], pair[1]); + return map; + } + + /** + * Adds `value` to `set`. + * + * @private + * @param {Object} set The set to modify. + * @param {*} value The value to add. + * @returns {Object} Returns `set`. + */ + function addSetEntry(set, value) { + // Don't return `set.add` because it's not chainable in IE 11. + set.add(value); + return set; + } + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ + function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; + } + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ + function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** Used for built-in method references. */ + var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = root['__core-js_shared__']; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString = objectProto.toString; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Buffer = moduleExports ? root.Buffer : undefined, + Symbol = root.Symbol, + Uint8Array = root.Uint8Array, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeKeys = overArg(Object.keys, Object); + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(root, 'DataView'), + Map = getNative(root, 'Map'), + Promise = getNative(root, 'Promise'), + Set = getNative(root, 'Set'), + WeakMap = getNative(root, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; + + /** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Hash(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ + function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + } + + /** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function hashDelete(key) { + return this.has(key) && delete this.__data__[key]; + } + + /** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; + } + + /** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function hashHas(key) { + var data = this.__data__; + return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); + } + + /** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ + function hashSet(key, value) { + var data = this.__data__; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; + } + + // Add methods to `Hash`. + Hash.prototype.clear = hashClear; + Hash.prototype['delete'] = hashDelete; + Hash.prototype.get = hashGet; + Hash.prototype.has = hashHas; + Hash.prototype.set = hashSet; + + /** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function ListCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ + function listCacheClear() { + this.__data__ = []; + } + + /** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + return true; + } + + /** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; + } + + /** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; + } + + /** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ + function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; + } + + // Add methods to `ListCache`. + ListCache.prototype.clear = listCacheClear; + ListCache.prototype['delete'] = listCacheDelete; + ListCache.prototype.get = listCacheGet; + ListCache.prototype.has = listCacheHas; + ListCache.prototype.set = listCacheSet; + + /** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function MapCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ + function mapCacheClear() { + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; + } + + /** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function mapCacheDelete(key) { + return getMapData(this, key)['delete'](key); + } + + /** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function mapCacheGet(key) { + return getMapData(this, key).get(key); + } + + /** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function mapCacheHas(key) { + return getMapData(this, key).has(key); + } + + /** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ + function mapCacheSet(key, value) { + getMapData(this, key).set(key, value); + return this; + } + + // Add methods to `MapCache`. + MapCache.prototype.clear = mapCacheClear; + MapCache.prototype['delete'] = mapCacheDelete; + MapCache.prototype.get = mapCacheGet; + MapCache.prototype.has = mapCacheHas; + MapCache.prototype.set = mapCacheSet; + + /** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Stack(entries) { + this.__data__ = new ListCache(entries); + } + + /** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ + function stackClear() { + this.__data__ = new ListCache; + } + + /** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function stackDelete(key) { + return this.__data__['delete'](key); + } + + /** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function stackGet(key) { + return this.__data__.get(key); + } + + /** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function stackHas(key) { + return this.__data__.has(key); + } + + /** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ + function stackSet(key, value) { + var cache = this.__data__; + if (cache instanceof ListCache) { + var pairs = cache.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + return this; + } + cache = this.__data__ = new MapCache(pairs); + } + cache.set(key, value); + return this; + } + + // Add methods to `Stack`. + Stack.prototype.clear = stackClear; + Stack.prototype['delete'] = stackDelete; + Stack.prototype.get = stackGet; + Stack.prototype.has = stackHas; + Stack.prototype.set = stackSet; + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + object[key] = value; + } + } + + /** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; + } + + /** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); + } + + /** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {boolean} [isFull] Specify a clone including symbols. + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, isFull, customizer, key, object, stack) { + var result; + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, baseClone, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); + }); + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + function baseCreate(proto) { + return isObject(proto) ? objectCreate(proto) : {}; + } + + /** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ + function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); + } + + /** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + return objectToString.call(value); + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ + function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var result = new buffer.constructor(buffer.length); + buffer.copy(result); + return result; + } + + /** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ + function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; + } + + /** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ + function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); + } + + /** + * Creates a clone of `map`. + * + * @private + * @param {Object} map The map to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned map. + */ + function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); + } + + /** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ + function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; + } + + /** + * Creates a clone of `set`. + * + * @private + * @param {Object} set The set to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned set. + */ + function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); + } + + /** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ + function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; + } + + /** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ + function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ + function copyObject(source, props, object, customizer) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + assignValue(object, key, newValue === undefined ? source[key] : newValue); + } + return object; + } + + /** + * Copies own symbol properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); + } + + /** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); + } + + /** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ + function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * Creates an array of the own enumerable symbol properties of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray; + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11, + // for data views in Edge < 14, and promises in Node.js. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = objectToString.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ + function initCloneArray(array) { + var length = array.length, + result = array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; + } + + /** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; + } + + /** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneByTag(object, tag, cloneFunc, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return cloneMap(object, isDeep, cloneFunc); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return cloneSet(object, isDeep, cloneFunc); + + case symbolTag: + return cloneSymbol(object); + } + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ + function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ + function cloneDeep(value) { + return baseClone(value, true, true); + } + + /** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ + function eq(value, other) { + return value === other || (value !== value && other !== other); + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); + } + + /** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ + var isBuffer = nativeIsBuffer || stubFalse; + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * This method returns a new empty array. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {Array} Returns the new empty array. + * @example + * + * var arrays = _.times(2, _.stubArray); + * + * console.log(arrays); + * // => [[], []] + * + * console.log(arrays[0] === arrays[1]); + * // => false + */ + function stubArray() { + return []; + } + + /** + * This method returns `false`. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {boolean} Returns `false`. + * @example + * + * _.times(2, _.stubFalse); + * // => [false, false] + */ + function stubFalse() { + return false; + } + + module.exports = cloneDeep; + }); + + var hasProperty = function has(object, key) { + return Object.prototype.hasOwnProperty.call(object, key); + }; + + var _apply; + + function _classPrivateFieldLooseBase$8(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } + + var id$8 = 0; + + function _classPrivateFieldLooseKey$8(name) { return "__private_" + id$8++ + "_" + name; } + + + + function insertReplacement(source, rx, replacement) { + const newParts = []; + source.forEach(chunk => { + // When the source contains multiple placeholders for interpolation, + // we should ignore chunks that are not strings, because those + // can be JSX objects and will be otherwise incorrectly turned into strings. + // Without this condition we’d get this: [object Object] hello [object Object] my