mirror of https://github.com/abpframework/abp.git
187 changed files with 15104 additions and 414 deletions
@ -0,0 +1,341 @@ |
|||
using System; |
|||
using System.Linq; |
|||
using System.Linq.Dynamic.Core; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Dtos; |
|||
using Volo.Abp.Auditing; |
|||
using Volo.Abp.Domain.Entities; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Linq; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.ObjectMapping; |
|||
|
|||
namespace Volo.Abp.Application.Services |
|||
{ |
|||
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey> |
|||
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, PagedAndSortedResultRequestDto> |
|||
where TEntity : class, IEntity |
|||
{ |
|||
protected AbstractKeyCrudAppService(IRepository<TEntity> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput> |
|||
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TEntityDto, TEntityDto> |
|||
where TEntity : class, IEntity |
|||
{ |
|||
protected AbstractKeyCrudAppService(IRepository<TEntity> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput> |
|||
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput> |
|||
where TEntity : class, IEntity |
|||
{ |
|||
protected AbstractKeyCrudAppService(IRepository<TEntity> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput> |
|||
: AbstractKeyCrudAppService<TEntity, TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput> |
|||
where TEntity : class, IEntity |
|||
{ |
|||
protected AbstractKeyCrudAppService(IRepository<TEntity> repository) |
|||
: base(repository) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected override TEntityDto MapToGetListOutputDto(TEntity entity) |
|||
{ |
|||
return MapToGetOutputDto(entity); |
|||
} |
|||
} |
|||
|
|||
public abstract class AbstractKeyCrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput> |
|||
: ApplicationService, |
|||
ICrudAppService<TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput> |
|||
where TEntity : class, IEntity |
|||
{ |
|||
public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; } |
|||
|
|||
protected IRepository<TEntity> Repository { get; } |
|||
|
|||
protected virtual string GetPolicyName { get; set; } |
|||
|
|||
protected virtual string GetListPolicyName { get; set; } |
|||
|
|||
protected virtual string CreatePolicyName { get; set; } |
|||
|
|||
protected virtual string UpdatePolicyName { get; set; } |
|||
|
|||
protected virtual string DeletePolicyName { get; set; } |
|||
|
|||
protected AbstractKeyCrudAppService(IRepository<TEntity> repository) |
|||
{ |
|||
Repository = repository; |
|||
AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance; |
|||
} |
|||
|
|||
public virtual async Task<TGetOutputDto> GetAsync(TKey id) |
|||
{ |
|||
await CheckGetPolicyAsync(); |
|||
|
|||
var entity = await GetEntityByIdAsync(id); |
|||
return MapToGetOutputDto(entity); |
|||
} |
|||
|
|||
public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input) |
|||
{ |
|||
await CheckGetListPolicyAsync(); |
|||
|
|||
var query = CreateFilteredQuery(input); |
|||
|
|||
var totalCount = await AsyncQueryableExecuter.CountAsync(query); |
|||
|
|||
query = ApplySorting(query, input); |
|||
query = ApplyPaging(query, input); |
|||
|
|||
var entities = await AsyncQueryableExecuter.ToListAsync(query); |
|||
|
|||
return new PagedResultDto<TGetListOutputDto>( |
|||
totalCount, |
|||
entities.Select(MapToGetListOutputDto).ToList() |
|||
); |
|||
} |
|||
|
|||
public virtual async Task<TGetOutputDto> CreateAsync(TCreateInput input) |
|||
{ |
|||
await CheckCreatePolicyAsync(); |
|||
|
|||
var entity = MapToEntity(input); |
|||
|
|||
TryToSetTenantId(entity); |
|||
|
|||
await Repository.InsertAsync(entity, autoSave: true); |
|||
|
|||
return MapToGetOutputDto(entity); |
|||
} |
|||
|
|||
public virtual async Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input) |
|||
{ |
|||
await CheckUpdatePolicyAsync(); |
|||
|
|||
var entity = await GetEntityByIdAsync(id); |
|||
//TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise
|
|||
MapToEntity(input, entity); |
|||
await Repository.UpdateAsync(entity, autoSave: true); |
|||
|
|||
return MapToGetOutputDto(entity); |
|||
} |
|||
|
|||
public virtual async Task DeleteAsync(TKey id) |
|||
{ |
|||
await CheckDeletePolicyAsync(); |
|||
|
|||
await DeleteByIdAsync(id); |
|||
} |
|||
|
|||
protected abstract Task DeleteByIdAsync(TKey id); |
|||
|
|||
protected abstract Task<TEntity> GetEntityByIdAsync(TKey id); |
|||
|
|||
protected virtual async Task CheckGetPolicyAsync() |
|||
{ |
|||
await CheckPolicyAsync(GetPolicyName); |
|||
} |
|||
|
|||
protected virtual async Task CheckGetListPolicyAsync() |
|||
{ |
|||
await CheckPolicyAsync(GetListPolicyName); |
|||
} |
|||
|
|||
protected virtual async Task CheckCreatePolicyAsync() |
|||
{ |
|||
await CheckPolicyAsync(CreatePolicyName); |
|||
} |
|||
|
|||
protected virtual async Task CheckUpdatePolicyAsync() |
|||
{ |
|||
await CheckPolicyAsync(UpdatePolicyName); |
|||
} |
|||
|
|||
protected virtual async Task CheckDeletePolicyAsync() |
|||
{ |
|||
await CheckPolicyAsync(DeletePolicyName); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Should apply sorting if needed.
|
|||
/// </summary>
|
|||
/// <param name="query">The query.</param>
|
|||
/// <param name="input">The input.</param>
|
|||
protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetListInput input) |
|||
{ |
|||
//Try to sort query if available
|
|||
if (input is ISortedResultRequest sortInput) |
|||
{ |
|||
if (!sortInput.Sorting.IsNullOrWhiteSpace()) |
|||
{ |
|||
return query.OrderBy(sortInput.Sorting); |
|||
} |
|||
} |
|||
|
|||
//IQueryable.Task requires sorting, so we should sort if Take will be used.
|
|||
if (input is ILimitedResultRequest) |
|||
{ |
|||
return ApplyDefaultSorting(query); |
|||
} |
|||
|
|||
//No sorting
|
|||
return query; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies sorting if no sorting specified but a limited result requested.
|
|||
/// </summary>
|
|||
/// <param name="query">The query.</param>
|
|||
protected virtual IQueryable<TEntity> ApplyDefaultSorting(IQueryable<TEntity> query) |
|||
{ |
|||
if (typeof(TEntity).IsAssignableTo<ICreationAuditedObject>()) |
|||
{ |
|||
return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime); |
|||
} |
|||
|
|||
throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyCrudAppService!"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Should apply paging if needed.
|
|||
/// </summary>
|
|||
/// <param name="query">The query.</param>
|
|||
/// <param name="input">The input.</param>
|
|||
protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetListInput input) |
|||
{ |
|||
//Try to use paging if available
|
|||
if (input is IPagedResultRequest pagedInput) |
|||
{ |
|||
return query.PageBy(pagedInput); |
|||
} |
|||
|
|||
//Try to limit query result if available
|
|||
if (input is ILimitedResultRequest limitedInput) |
|||
{ |
|||
return query.Take(limitedInput.MaxResultCount); |
|||
} |
|||
|
|||
//No paging
|
|||
return query; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// This method should create <see cref="IQueryable{TEntity}"/> based on given input.
|
|||
/// It should filter query if needed, but should not do sorting or paging.
|
|||
/// Sorting should be done in <see cref="ApplySorting"/> and paging should be done in <see cref="ApplyPaging"/>
|
|||
/// methods.
|
|||
/// </summary>
|
|||
/// <param name="input">The input.</param>
|
|||
protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetListInput input) |
|||
{ |
|||
return Repository; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps <see cref="TEntity"/> to <see cref="TGetOutputDto"/>.
|
|||
/// It uses <see cref="IObjectMapper"/> by default.
|
|||
/// It can be overriden for custom mapping.
|
|||
/// </summary>
|
|||
protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity) |
|||
{ |
|||
return ObjectMapper.Map<TEntity, TGetOutputDto>(entity); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps <see cref="TEntity"/> to <see cref="TGetListOutputDto"/>.
|
|||
/// It uses <see cref="IObjectMapper"/> by default.
|
|||
/// It can be overriden for custom mapping.
|
|||
/// </summary>
|
|||
protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity) |
|||
{ |
|||
return ObjectMapper.Map<TEntity, TGetListOutputDto>(entity); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps <see cref="TCreateInput"/> to <see cref="TEntity"/> to create a new entity.
|
|||
/// It uses <see cref="IObjectMapper"/> by default.
|
|||
/// It can be overriden for custom mapping.
|
|||
/// </summary>
|
|||
protected virtual TEntity MapToEntity(TCreateInput createInput) |
|||
{ |
|||
var entity = ObjectMapper.Map<TCreateInput, TEntity>(createInput); |
|||
SetIdForGuids(entity); |
|||
return entity; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets Id value for the entity if <see cref="TKey"/> is <see cref="Guid"/>.
|
|||
/// It's used while creating a new entity.
|
|||
/// </summary>
|
|||
protected virtual void SetIdForGuids(TEntity entity) |
|||
{ |
|||
var entityWithGuidId = entity as IEntity<Guid>; |
|||
|
|||
if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
EntityHelper.TrySetId( |
|||
entityWithGuidId, |
|||
() => GuidGenerator.Create(), |
|||
true |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps <see cref="TUpdateInput"/> to <see cref="TEntity"/> to update the entity.
|
|||
/// It uses <see cref="IObjectMapper"/> by default.
|
|||
/// It can be overriden for custom mapping.
|
|||
/// </summary>
|
|||
protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity) |
|||
{ |
|||
ObjectMapper.Map(updateInput, entity); |
|||
} |
|||
|
|||
protected virtual void TryToSetTenantId(TEntity entity) |
|||
{ |
|||
if (entity is IMultiTenant && HasTenantIdProperty(entity)) |
|||
{ |
|||
var tenantId = CurrentTenant.Id; |
|||
|
|||
if (!tenantId.HasValue) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)); |
|||
|
|||
if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
propertyInfo.SetValue(entity, tenantId); |
|||
} |
|||
} |
|||
|
|||
protected virtual bool HasTenantIdProperty(TEntity entity) |
|||
{ |
|||
return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.TestApp.Domain; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.TestApp.Application.Dto; |
|||
|
|||
namespace Volo.Abp.TestApp.Application |
|||
{ |
|||
//This is especially used to test the AbstractKeyCrudAppService
|
|||
public class DistrictAppService : AbstractKeyCrudAppService<District, DistrictDto, DistrictKey> |
|||
{ |
|||
public DistrictAppService(IRepository<District> repository) |
|||
: base(repository) |
|||
{ |
|||
} |
|||
|
|||
protected override async Task DeleteByIdAsync(DistrictKey id) |
|||
{ |
|||
await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name); |
|||
} |
|||
|
|||
protected override async Task<District> GetEntityByIdAsync(DistrictKey id) |
|||
{ |
|||
return await AsyncQueryableExecuter.FirstOrDefaultAsync( |
|||
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.TestApp.Application |
|||
{ |
|||
public class DistrictKey |
|||
{ |
|||
public Guid CityId { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace Volo.Abp.TestApp.Application.Dto |
|||
{ |
|||
public class DistrictDto : EntityDto |
|||
{ |
|||
public Guid CityId { get; set; } |
|||
|
|||
public string Name { get; set; } |
|||
|
|||
public int Population { get; set; } |
|||
} |
|||
} |
|||
@ -1,7 +1,22 @@ |
|||
namespace Volo.Abp.TenantManagement |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Text.RegularExpressions; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Localization; |
|||
using Volo.Abp.TenantManagement.Localization; |
|||
|
|||
namespace Volo.Abp.TenantManagement |
|||
{ |
|||
public class TenantCreateDto : TenantCreateOrUpdateDtoBase |
|||
{ |
|||
[Required] |
|||
[EmailAddress] |
|||
[MaxLength(256)] |
|||
public string AdminEmailAddress { get; set; } |
|||
|
|||
|
|||
[Required] |
|||
[MaxLength(128)] |
|||
public string AdminPassword { get; set; } |
|||
} |
|||
} |
|||
@ -1,7 +1,11 @@ |
|||
namespace Volo.Abp.TenantManagement |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Volo.Abp.TenantManagement |
|||
{ |
|||
public abstract class TenantCreateOrUpdateDtoBase |
|||
{ |
|||
[Required] |
|||
[StringLength(TenantConsts.MaxNameLength)] |
|||
public string Name { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
{ |
|||
"extends": ["airbnb", "prettier", "prettier/react"], |
|||
"parser": "babel-eslint", |
|||
"env": { |
|||
"jest": true |
|||
}, |
|||
"rules": { |
|||
"no-use-before-define": 0, |
|||
"react/jsx-filename-extension": 0, |
|||
"react/prop-types": ["error", { "ignore": ["navigation", "children"] }], |
|||
"react/require-default-props": 0, |
|||
"react/jsx-props-no-spreading": 0, |
|||
"react/forbid-prop-types": 0, |
|||
"import/prefer-default-export": 0, |
|||
"comma-dangle": 0, |
|||
"no-underscore-dangle": 1, |
|||
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], |
|||
"no-param-reassign": 0, |
|||
"operator-linebreak": 0, |
|||
"global-require": 0 |
|||
}, |
|||
"globals": { |
|||
"fetch": false |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
node_modules/**/* |
|||
.expo/* |
|||
npm-debug.* |
|||
*.jks |
|||
*.p8 |
|||
*.p12 |
|||
*.key |
|||
*.mobileprovision |
|||
*.orig.* |
|||
web-build/ |
|||
web-report/ |
|||
|
|||
# macOS |
|||
.DS_Store |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"trailingComma": "all", |
|||
"singleQuote": true, |
|||
"jsxSingleQuote": false, |
|||
"printWidth": 100, |
|||
"semi": true, |
|||
"jsxBracketSameLine": true |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
{ |
|||
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
import { StyleProvider } from 'native-base'; |
|||
import React from 'react'; |
|||
import { enableScreens } from 'react-native-screens'; |
|||
import { Provider } from 'react-redux'; |
|||
import { PersistGate } from 'redux-persist/integration/react'; |
|||
import AppContainer from './src/components/AppContainer/AppContainer'; |
|||
import { store, persistor } from './src/store'; |
|||
import getTheme from './src/theme/components'; |
|||
import { activeTheme } from './src/theme/variables'; |
|||
import { initAPIInterceptor } from './src/interceptors/APIInterceptor'; |
|||
|
|||
enableScreens(); |
|||
initAPIInterceptor(store); |
|||
|
|||
export default function App() { |
|||
return ( |
|||
<Provider store={store}> |
|||
<PersistGate loading={null} persistor={persistor}> |
|||
<StyleProvider style={getTheme(activeTheme)}> |
|||
<AppContainer /> |
|||
</StyleProvider> |
|||
</PersistGate> |
|||
</Provider> |
|||
); |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
const ENV = { |
|||
dev: { |
|||
apiUrl: 'http://localhost:44305', |
|||
oAuthConfig: { |
|||
issuer: 'http://localhost:44305', |
|||
clientId: 'MyProjectName_App', |
|||
clientSecret: '1q2w3e*', |
|||
scope: 'MyProjectName', |
|||
}, |
|||
localization: { |
|||
defaultResourceName: 'MyProjectName', |
|||
}, |
|||
}, |
|||
prod: { |
|||
apiUrl: 'http://localhost:44305', |
|||
oAuthConfig: { |
|||
issuer: 'http://localhost:44305', |
|||
clientId: 'MyProjectName_App', |
|||
clientSecret: '1q2w3e*', |
|||
scope: 'MyProjectName', |
|||
}, |
|||
localization: { |
|||
defaultResourceName: 'MyProjectName', |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export const getEnvVars = () => { |
|||
// eslint-disable-next-line no-undef
|
|||
return __DEV__ ? ENV.dev : ENV.prod; |
|||
}; |
|||
@ -0,0 +1,30 @@ |
|||
{ |
|||
"expo": { |
|||
"name": "MyProjectName", |
|||
"slug": "MyProjectName", |
|||
"privacy": "public", |
|||
"sdkVersion": "36.0.0", |
|||
"platforms": ["ios", "android", "web"], |
|||
"version": "1.0.0", |
|||
"orientation": "portrait", |
|||
"icon": "./assets/icon.png", |
|||
"splash": { |
|||
"image": "./assets/splash.png", |
|||
"resizeMode": "cover", |
|||
"backgroundColor": "#38003c" |
|||
}, |
|||
"updates": { |
|||
"fallbackToCacheTimeout": 0 |
|||
}, |
|||
"assetBundlePatterns": ["**/*"], |
|||
"ios": { |
|||
"supportsTablet": true, |
|||
"bundleIdentifier": "com.MyCompanyName.MyProjectName", |
|||
"buildNumber": "1.0.0" |
|||
}, |
|||
"android": { |
|||
"package": "com.MyCompanyName.MyProjectName", |
|||
"versionCode": 1 |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 255 B |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 93 KiB |
@ -0,0 +1,6 @@ |
|||
module.exports = function(api) { |
|||
api.cache(true); |
|||
return { |
|||
presets: ['babel-preset-expo'], |
|||
}; |
|||
}; |
|||
@ -0,0 +1,62 @@ |
|||
{ |
|||
"main": "node_modules/expo/AppEntry.js", |
|||
"scripts": { |
|||
"start": "expo start", |
|||
"android": "expo start --android", |
|||
"ios": "expo start --ios", |
|||
"web": "expo start --web", |
|||
"eject": "expo eject", |
|||
"lint": "eslint *.js **/*.js", |
|||
"lint:fix": "yarn lint --fix" |
|||
}, |
|||
"dependencies": { |
|||
"@expo/vector-icons": "^10.0.6", |
|||
"@react-native-community/masked-view": "0.1.5", |
|||
"@react-navigation/drawer": "^5.1.1", |
|||
"@react-navigation/native": "^5.0.9", |
|||
"@react-navigation/stack": "^5.1.1", |
|||
"@reduxjs/toolkit": "^1.2.3", |
|||
"axios": "^0.19.2", |
|||
"color": "^3.1.2", |
|||
"expo": "~36.0.0", |
|||
"expo-constants": "~8.0.0", |
|||
"expo-font": "~8.0.0", |
|||
"formik": "^2.1.2", |
|||
"i18n-js": "^3.5.1", |
|||
"lodash": "^4.17.15", |
|||
"native-base": "^2.13.8", |
|||
"prop-types": "^15.7.2", |
|||
"react": "~16.9.0", |
|||
"react-dom": "~16.9.0", |
|||
"react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz", |
|||
"react-native-gesture-handler": "~1.5.0", |
|||
"react-native-reanimated": "~1.4.0", |
|||
"react-native-safe-area-context": "0.6.0", |
|||
"react-native-safe-area-view": "^1.0.0", |
|||
"react-native-screens": "2.0.0-alpha.12", |
|||
"react-native-web": "~0.11.7", |
|||
"react-redux": "^7.1.3", |
|||
"redux-persist": "^6.0.0", |
|||
"redux-saga": "^1.1.3", |
|||
"reselect": "^4.0.0", |
|||
"yup": "^0.28.0" |
|||
}, |
|||
"devDependencies": { |
|||
"@babel/core": "^7.0.0", |
|||
"@types/i18n-js": "^3.0.1", |
|||
"@types/react": "~16.9.0", |
|||
"@types/react-native": "~0.60.23", |
|||
"@types/react-redux": "^7.1.7", |
|||
"@types/yup": "^0.26.29", |
|||
"babel-eslint": "^10.0.3", |
|||
"babel-preset-expo": "~8.0.0", |
|||
"eslint": "^6.8.0", |
|||
"eslint-config-airbnb": "^18.0.1", |
|||
"eslint-config-prettier": "^6.10.0", |
|||
"eslint-plugin-import": "^2.20.1", |
|||
"eslint-plugin-jsx-a11y": "^6.2.3", |
|||
"eslint-plugin-react": "^7.18.3", |
|||
"prettier": "^1.19.1" |
|||
}, |
|||
"private": true |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import axios from 'axios'; |
|||
import { getEnvVars } from '../../Environment'; |
|||
|
|||
const { apiUrl } = getEnvVars(); |
|||
|
|||
const axiosInstance = axios.create({ |
|||
baseURL: apiUrl, |
|||
}); |
|||
|
|||
export default axiosInstance; |
|||
@ -0,0 +1,41 @@ |
|||
import api from './API'; |
|||
import { getEnvVars } from '../../Environment'; |
|||
|
|||
const { oAuthConfig } = getEnvVars(); |
|||
|
|||
export const login = ({ username, password }) => { |
|||
// eslint-disable-next-line no-undef
|
|||
const formData = new FormData(); |
|||
formData.append('username', username); |
|||
formData.append('password', password); |
|||
formData.append('grant_type', 'password'); |
|||
formData.append('scope', `${oAuthConfig.scope} offline_access`); |
|||
formData.append('client_id', oAuthConfig.clientId); |
|||
formData.append('client_secret', oAuthConfig.clientSecret); |
|||
|
|||
return api({ |
|||
method: 'POST', |
|||
url: '/connect/token', |
|||
headers: { 'Content-Type': 'multipart/form-data' }, |
|||
data: formData, |
|||
baseURL: oAuthConfig.issuer, |
|||
}).then(({ data }) => data); |
|||
}; |
|||
|
|||
export const logout = () => |
|||
api({ |
|||
method: 'GET', |
|||
url: '/api/account/logout', |
|||
}).then(({ data }) => data); |
|||
|
|||
export const getTenant = tenantName => |
|||
api({ |
|||
method: 'GET', |
|||
url: `/api/abp/multi-tenancy/tenants/by-name/${tenantName}`, |
|||
}).then(({ data }) => data); |
|||
|
|||
export const getTenantById = tenantId => |
|||
api({ |
|||
method: 'GET', |
|||
url: `/api/abp/multi-tenancy/tenants/by-id/${tenantId}`, |
|||
}).then(({ data }) => data); |
|||
@ -0,0 +1,30 @@ |
|||
import i18n from 'i18n-js'; |
|||
import api from './API'; |
|||
|
|||
export const getApplicationConfiguration = () => |
|||
api |
|||
.get('/api/abp/application-configuration') |
|||
.then(({ data }) => data) |
|||
.then(async config => { |
|||
const { cultureName } = config.localization.currentCulture; |
|||
i18n.locale = cultureName; |
|||
|
|||
Object.keys(config.localization.values).forEach(key => { |
|||
const resource = config.localization.values[key]; |
|||
|
|||
if (typeof resource !== 'object') return; |
|||
|
|||
Object.keys(resource).forEach(key2 => { |
|||
if (/'{|{/g.test(resource[key2])) { |
|||
resource[key2] = resource[key2].replace(/'{|{/g, '{{').replace(/}'|}/g, '}}'); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
i18n.translations[cultureName] = { |
|||
...config.localization.values, |
|||
...(i18n.translations[cultureName] || {}), |
|||
}; |
|||
|
|||
return config; |
|||
}); |
|||
@ -0,0 +1,26 @@ |
|||
import api from './API'; |
|||
|
|||
export const getProfileDetail = () => api.get('/api/identity/my-profile').then(({ data }) => data); |
|||
|
|||
export const getAllRoles = () => api.get('/api/identity/roles/all').then(({ data }) => data.items); |
|||
|
|||
export const getUserRoles = id => |
|||
api.get(`/api/identity/users/${id}/roles`).then(({ data }) => data.items); |
|||
|
|||
export const getUsers = (params = { maxResultCount: 10, skipCount: 0 }) => |
|||
api.get('/api/identity/users', { params }).then(({ data }) => data); |
|||
|
|||
export const getUserById = id => api.get(`/api/identity/users/${id}`).then(({ data }) => data); |
|||
|
|||
export const createUser = body => api.post('/api/identity/users', body).then(({ data }) => data); |
|||
|
|||
export const updateUser = (body, id) => |
|||
api.put(`/api/identity/users/${id}`, body).then(({ data }) => data); |
|||
|
|||
export const removeUser = id => api.delete(`/api/identity/users/${id}`); |
|||
|
|||
export const updateProfileDetail = body => |
|||
api.put('/api/identity/my-profile', body).then(({ data }) => data); |
|||
|
|||
export const changePassword = body => |
|||
api.post('/api/identity/my-profile/change-password', body).then(({ data }) => data); |
|||
@ -0,0 +1,21 @@ |
|||
import api from './API'; |
|||
|
|||
export function getTenants(params = {}) { |
|||
return api.get('/api/multi-tenancy/tenants', { params }).then(({ data }) => data); |
|||
} |
|||
|
|||
export function createTenant(body) { |
|||
return api.post('/api/multi-tenancy/tenants', body).then(({ data }) => data); |
|||
} |
|||
|
|||
export function getTenantById(id) { |
|||
return api.get(`/api/multi-tenancy/tenants/${id}`).then(({ data }) => data); |
|||
} |
|||
|
|||
export function updateTenant(body, id) { |
|||
return api.put(`/api/multi-tenancy/tenants/${id}`, body).then(({ data }) => data); |
|||
} |
|||
|
|||
export function removeTenant(id) { |
|||
return api.delete(`/api/multi-tenancy/tenants/${id}`).then(({ data }) => data); |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import * as Font from 'expo-font'; |
|||
import i18n from 'i18n-js'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { useEffect, useState, useMemo } from 'react'; |
|||
import { Platform, StatusBar } from 'react-native'; |
|||
import { NavigationContainer } from '@react-navigation/native'; |
|||
import { Root } from 'native-base'; |
|||
import Loading from '../Loading/Loading'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import { createLanguageSelector } from '../../store/selectors/AppSelectors'; |
|||
import { createTokenSelector } from '../../store/selectors/PersistentStorageSelectors'; |
|||
import AppActions from '../../store/actions/AppActions'; |
|||
import PersistentStorageActions from '../../store/actions/PersistentStorageActions'; |
|||
import { LocalizationContext } from '../../contexts/LocalizationContext'; |
|||
import { isTokenValid } from '../../utils/TokenUtils'; |
|||
import DrawerNavigator from '../../navigators/DrawerNavigator'; |
|||
import AuthNavigator from '../../navigators/AuthNavigator'; |
|||
import { getEnvVars } from '../../../Environment'; |
|||
|
|||
const { localization } = getEnvVars(); |
|||
|
|||
i18n.defaultSeparator = '::'; |
|||
|
|||
const cloneT = i18n.t; |
|||
i18n.t = (key, ...args) => { |
|||
if (key.slice(0, 2) === '::') { |
|||
key = localization.defaultResourceName + key; |
|||
} |
|||
return cloneT(key, ...args); |
|||
}; |
|||
|
|||
function AppContainer({ language, fetchAppConfig, token, setToken }) { |
|||
const platform = Platform.OS; |
|||
const [isReady, setIsReady] = useState(false); |
|||
|
|||
const localizationContext = useMemo( |
|||
() => ({ |
|||
t: i18n.t, |
|||
locale: (language || {}).cultureName, |
|||
}), |
|||
[language], |
|||
); |
|||
|
|||
const isValid = useMemo(() => isTokenValid(token), [token]); |
|||
|
|||
useEffect(() => { |
|||
if (!isValid && token && token.access_token) { |
|||
setToken({}); |
|||
} |
|||
}, [isValid]); |
|||
|
|||
useEffect(() => { |
|||
fetchAppConfig(); |
|||
|
|||
Font.loadAsync({ |
|||
Roboto: require('native-base/Fonts/Roboto.ttf'), |
|||
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'), |
|||
...Ionicons.font, |
|||
}).then(() => setIsReady(true)); |
|||
}, []); |
|||
|
|||
return ( |
|||
<> |
|||
<StatusBar barStyle={platform === 'ios' ? 'dark-content' : 'light-content'} /> |
|||
<Root> |
|||
{isReady && language ? ( |
|||
<LocalizationContext.Provider value={localizationContext}> |
|||
<NavigationContainer> |
|||
{isValid ? <DrawerNavigator /> : <AuthNavigator />} |
|||
</NavigationContainer> |
|||
</LocalizationContext.Provider> |
|||
) : null} |
|||
</Root> |
|||
<Loading /> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
AppContainer.propTypes = { |
|||
language: PropTypes.object, |
|||
token: PropTypes.object.isRequired, |
|||
fetchAppConfig: PropTypes.func.isRequired, |
|||
setToken: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default connectToRedux({ |
|||
component: AppContainer, |
|||
stateProps: state => ({ |
|||
language: createLanguageSelector()(state), |
|||
token: createTokenSelector()(state), |
|||
}), |
|||
dispatchProps: { |
|||
fetchAppConfig: AppActions.fetchAppConfigAsync, |
|||
setToken: PersistentStorageActions.setToken, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,139 @@ |
|||
import { useFocusEffect } from '@react-navigation/native'; |
|||
import i18n from 'i18n-js'; |
|||
import { connectStyle, Icon, Input, InputGroup, Item, List, Spinner, Text } from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { forwardRef, useCallback, useEffect, useState } from 'react'; |
|||
import { RefreshControl, StyleSheet, View } from 'react-native'; |
|||
import LoadingActions from '../../store/actions/LoadingActions'; |
|||
import { activeTheme } from '../../theme/variables'; |
|||
import { debounce } from '../../utils/Debounce'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import LoadingButton from '../LoadingButton/LoadingButton'; |
|||
|
|||
function DataList({ |
|||
style, |
|||
navigation, |
|||
fetchFn, |
|||
render, |
|||
maxResultCount = 15, |
|||
debounceTime = 350, |
|||
...props |
|||
}) { |
|||
const [records, setRecords] = useState([]); |
|||
const [totalCount, setTotalCount] = useState(0); |
|||
const [loading, setLoading] = useState(false); |
|||
const [searchLoading, setSearchLoading] = useState(false); |
|||
const [buttonLoading, setButtonLoading] = useState(false); |
|||
const [skipCount, setSkipCount] = useState(0); |
|||
const [filter, setFilter] = useState(''); |
|||
|
|||
const fetch = (skip = 0, isRefreshingActive = true) => { |
|||
if (isRefreshingActive) setLoading(true); |
|||
return fetchFn({ filter, maxResultCount, skipCount: skip }) |
|||
.then(({ items, totalCount: total }) => { |
|||
setTotalCount(total); |
|||
setRecords(skip ? [...records, ...items] : items); |
|||
setSkipCount(skip); |
|||
}) |
|||
.finally(() => { |
|||
if (isRefreshingActive) setLoading(false); |
|||
}); |
|||
}; |
|||
|
|||
const fetchPartial = () => { |
|||
if (loading || records.length === totalCount) return; |
|||
|
|||
setButtonLoading(true); |
|||
fetch(skipCount + maxResultCount, false).finally(() => setButtonLoading(false)); |
|||
}; |
|||
|
|||
useFocusEffect( |
|||
useCallback(() => { |
|||
setSkipCount(0); |
|||
fetch(0, false); |
|||
}, []), |
|||
); |
|||
|
|||
useEffect(() => { |
|||
function searchFetch() { |
|||
setSearchLoading(true); |
|||
return fetch(0, false).finally(() => setTimeout(() => setSearchLoading(false), 150)); |
|||
} |
|||
debounce(searchFetch, debounceTime)(); |
|||
}, [filter]); |
|||
|
|||
return ( |
|||
<> |
|||
<Item placeholderLabel style={{ backgroundColor: '#fff' }}> |
|||
<InputGroup style={{ marginLeft: 10 }}> |
|||
<Input |
|||
placeholder={i18n.t('AbpUi::PagerSearch')} |
|||
style={{ padding: 0, margin: 0 }} |
|||
returnKeyType="done" |
|||
value={filter} |
|||
onChangeText={setFilter} |
|||
/> |
|||
{searchLoading ? ( |
|||
<View> |
|||
<Spinner style={style.spinner} color={style.spinner.color} /> |
|||
</View> |
|||
) : ( |
|||
<Icon style={{ fontSize: 20, marginRight: 15 }} name="ios-search" /> |
|||
)} |
|||
</InputGroup> |
|||
</Item> |
|||
<View style={style.container}> |
|||
<List |
|||
showsVerticalScrollIndicator |
|||
scrollEnabled |
|||
refreshControl={<RefreshControl refreshing={loading} onRefresh={fetch} />} |
|||
dataArray={records} |
|||
renderRow={(data, sectionID, rowId, ...args) => ( |
|||
<> |
|||
{render(data, sectionID, rowId, ...args)} |
|||
{rowId + 1 === skipCount + maxResultCount && totalCount > records.length ? ( |
|||
<View style={{ justifyContent: 'center', alignItems: 'center' }}> |
|||
<LoadingButton loading={buttonLoading} onPress={() => fetchPartial()}> |
|||
<Text>{i18n.t('AbpUi::LoadMore')}</Text> |
|||
</LoadingButton> |
|||
</View> |
|||
) : null} |
|||
</> |
|||
)} |
|||
{...props} |
|||
/> |
|||
</View> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
DataList.propTypes = { |
|||
...List.propTypes, |
|||
style: PropTypes.any.isRequired, |
|||
fetchFn: PropTypes.func.isRequired, |
|||
render: PropTypes.func.isRequired, |
|||
maxResultCount: PropTypes.number, |
|||
debounceTime: PropTypes.number, |
|||
}; |
|||
|
|||
const styles = StyleSheet.create({ |
|||
container: { flex: 1 }, |
|||
list: {}, |
|||
spinner: { |
|||
transform: [{ scale: 0.5 }], |
|||
position: 'absolute', |
|||
right: 8, |
|||
top: -40, |
|||
color: activeTheme.brandInfo, |
|||
}, |
|||
}); |
|||
|
|||
const Forwarded = forwardRef((props, ref) => <DataList {...props} forwardedRef={ref} />); |
|||
|
|||
export default connectToRedux({ |
|||
component: connectStyle('ABP.DataList', styles)(Forwarded), |
|||
dispatchProps: { |
|||
startLoading: LoadingActions.start, |
|||
stopLoading: LoadingActions.stop, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,121 @@ |
|||
import { Text, View, List, ListItem, Left, Icon, Body } from 'native-base'; |
|||
import React from 'react'; |
|||
import { Image, StyleSheet } from 'react-native'; |
|||
import SafeAreaView from 'react-native-safe-area-view'; |
|||
import i18n from 'i18n-js'; |
|||
import PropTypes from 'prop-types'; |
|||
import Constants from 'expo-constants'; |
|||
import { withPermission } from '../../hocs/PermissionHOC'; |
|||
|
|||
const screens = { |
|||
Home: { label: '::Menu:Home', iconName: 'home' }, |
|||
Users: { |
|||
label: 'AbpIdentity::Users', |
|||
iconName: 'contacts', |
|||
requiredPolicy: 'AbpIdentity.Users', |
|||
}, |
|||
Tenants: { |
|||
label: 'AbpTenantManagement::Tenants', |
|||
iconName: 'people', |
|||
requiredPolicy: 'AbpTenantManagement.Tenants', |
|||
}, |
|||
Settings: { label: 'AbpSettingManagement::Settings', iconName: 'cog' }, |
|||
}; |
|||
|
|||
const ListItemWithPermission = withPermission(ListItem); |
|||
|
|||
function DrawerContent({ navigation, state: { routeNames, index: currentScreenIndex } }) { |
|||
const navigate = screen => { |
|||
navigation.navigate(screen); |
|||
navigation.closeDrawer(); |
|||
}; |
|||
|
|||
return ( |
|||
<View style={styles.container}> |
|||
<SafeAreaView style={styles.container} forceInset={{ top: 'always', horizontal: 'never' }}> |
|||
<View style={styles.headerView}> |
|||
<Image style={styles.logo} source={require('../../../assets/logo.png')} /> |
|||
</View> |
|||
<List |
|||
dataArray={routeNames} |
|||
keyExtractor={item => item} |
|||
renderRow={name => ( |
|||
<ListItemWithPermission |
|||
icon |
|||
key={name} |
|||
policyKey={screens[name].requiredPolicy} |
|||
selected={name === routeNames[currentScreenIndex]} |
|||
onPress={() => navigate(name)} |
|||
style={{ |
|||
...styles.navItem, |
|||
backgroundColor: name === routeNames[currentScreenIndex] ? '#38003c' : '#f2f2f2', |
|||
}}> |
|||
<Left> |
|||
<Icon |
|||
dark={name !== routeNames[currentScreenIndex]} |
|||
light={name === routeNames[currentScreenIndex]} |
|||
name={screens[name].iconName} |
|||
/> |
|||
</Left> |
|||
<Body style={{ borderBottomWidth: 0 }}> |
|||
<Text |
|||
style={{ |
|||
color: name === routeNames[currentScreenIndex] ? '#fff' : '#000', |
|||
}}> |
|||
{i18n.t(screens[name].label)} |
|||
</Text> |
|||
</Body> |
|||
</ListItemWithPermission> |
|||
)} |
|||
/> |
|||
</SafeAreaView> |
|||
<View style={styles.footer}> |
|||
<Text note style={styles.copyRight}> |
|||
© MyProjectName |
|||
</Text> |
|||
<Text note style={styles.version}> |
|||
v{Constants.manifest.version} |
|||
</Text> |
|||
</View> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
container: { |
|||
flexGrow: 1, |
|||
}, |
|||
logo: { |
|||
marginTop: 20, |
|||
marginBottom: 15, |
|||
}, |
|||
headerView: { |
|||
borderBottomWidth: 1, |
|||
borderColor: '#eee', |
|||
alignItems: 'center', |
|||
}, |
|||
navItem: { |
|||
marginLeft: 0, |
|||
marginBottom: 3, |
|||
paddingLeft: 10, |
|||
width: '100%', |
|||
backgroundColor: '#f2f2f2', |
|||
}, |
|||
footer: { |
|||
backgroundColor: '#eee', |
|||
flexDirection: 'row', |
|||
justifyContent: 'space-between', |
|||
}, |
|||
copyRight: { |
|||
margin: 15, |
|||
}, |
|||
version: { |
|||
margin: 15, |
|||
}, |
|||
}); |
|||
|
|||
DrawerContent.propTypes = { |
|||
state: PropTypes.object.isRequired, |
|||
}; |
|||
|
|||
export default DrawerContent; |
|||
@ -0,0 +1,82 @@ |
|||
import React, { forwardRef } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { Button, Text, connectStyle } from 'native-base'; |
|||
import { View, StyleSheet, Alert } from 'react-native'; |
|||
import i18n from 'i18n-js'; |
|||
|
|||
function FormButtons({ |
|||
style, |
|||
submit, |
|||
remove, |
|||
removeMessage, |
|||
isRemoveDisabled, |
|||
isSubmitDisabled, |
|||
isShowRemove = false, |
|||
isShowSubmit = true, |
|||
}) { |
|||
const confirmation = () => { |
|||
Alert.alert( |
|||
i18n.t('AbpUi::AreYouSure'), |
|||
removeMessage, |
|||
[ |
|||
{ |
|||
text: i18n.t('AbpUi::Cancel'), |
|||
style: 'cancel', |
|||
}, |
|||
{ text: i18n.t('AbpUi::Yes'), onPress: () => remove() }, |
|||
], |
|||
{ cancelable: true }, |
|||
); |
|||
}; |
|||
|
|||
return ( |
|||
<View style={style.container}> |
|||
{isShowRemove ? ( |
|||
<Button |
|||
abpButton |
|||
danger |
|||
style={{ flex: 1, borderRadius: 0 }} |
|||
onPress={() => confirmation()} |
|||
disabled={isRemoveDisabled}> |
|||
<Text>{i18n.t('AbpIdentity::Delete')}</Text> |
|||
</Button> |
|||
) : null} |
|||
{isShowSubmit ? ( |
|||
<Button |
|||
abpButton |
|||
primary |
|||
style={{ flex: 1, borderRadius: 0 }} |
|||
onPress={submit} |
|||
disabled={isSubmitDisabled}> |
|||
<Text>{i18n.t('AbpIdentity::Save')}</Text> |
|||
</Button> |
|||
) : null} |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
FormButtons.propTypes = { |
|||
submit: PropTypes.func.isRequired, |
|||
remove: PropTypes.func, |
|||
removeMessage: PropTypes.string, |
|||
style: PropTypes.any, |
|||
isRemoveDisabled: PropTypes.bool, |
|||
isSubmitDisabled: PropTypes.bool, |
|||
isShowRemove: PropTypes.bool, |
|||
isShowSubmit: PropTypes.bool, |
|||
}; |
|||
|
|||
const styles = StyleSheet.create({ |
|||
container: { |
|||
width: '100%', |
|||
justifyContent: 'center', |
|||
alignItems: 'center', |
|||
position: 'absolute', |
|||
bottom: 0, |
|||
flexDirection: 'row', |
|||
}, |
|||
}); |
|||
|
|||
const Forwarded = forwardRef((props, ref) => <FormButtons {...props} forwardedRef={ref} />); |
|||
|
|||
export default connectStyle('ABP.FormButtons', styles)(Forwarded); |
|||
@ -0,0 +1,63 @@ |
|||
import React, { forwardRef } from 'react'; |
|||
import { Spinner, View, connectStyle } from 'native-base'; |
|||
import { StyleSheet } from 'react-native'; |
|||
import PropTypes from 'prop-types'; |
|||
import { activeTheme } from '../../theme/variables'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import { |
|||
createLoadingSelector, |
|||
createOpacitySelector, |
|||
} from '../../store/selectors/LoadingSelectors'; |
|||
|
|||
function Loading({ style, loading, opacity }) { |
|||
return loading ? ( |
|||
<View style={style.container}> |
|||
<View |
|||
style={{ |
|||
...style.backdrop, |
|||
opacity: opacity || 0.6, |
|||
}} |
|||
/> |
|||
<Spinner style={style.spinner} color={style.spinner.color} /> |
|||
</View> |
|||
) : null; |
|||
} |
|||
const Forwarded = forwardRef((props, ref) => <Loading {...props} forwardedRef={ref} />); |
|||
|
|||
const backdropStyle = { |
|||
position: 'absolute', |
|||
top: 0, |
|||
left: 0, |
|||
width: '100%', |
|||
height: '100%', |
|||
backgroundColor: '#fff', |
|||
}; |
|||
|
|||
export const styles = StyleSheet.create({ |
|||
container: { |
|||
...backdropStyle, |
|||
backgroundColor: 'transparent', |
|||
zIndex: activeTheme.zIndex.indicator, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
backdrop: backdropStyle, |
|||
spinner: { |
|||
color: activeTheme.brandPrimary, |
|||
fontSize: 100, |
|||
}, |
|||
}); |
|||
|
|||
Loading.propTypes = { |
|||
style: PropTypes.objectOf(PropTypes.any), |
|||
loading: PropTypes.bool, |
|||
opacity: PropTypes.number, |
|||
}; |
|||
|
|||
export default connectToRedux({ |
|||
component: connectStyle('ABP.Loading', styles)(Forwarded), |
|||
stateProps: state => ({ |
|||
loading: createLoadingSelector()(state), |
|||
opacity: createOpacitySelector()(state), |
|||
}), |
|||
}); |
|||
@ -0,0 +1,31 @@ |
|||
import { Button, connectStyle, Spinner } from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { forwardRef } from 'react'; |
|||
import { StyleSheet } from 'react-native'; |
|||
|
|||
function LoadingButton({ loading = false, style, children, ...props }) { |
|||
return ( |
|||
<Button style={style.button} {...props}> |
|||
{children} |
|||
{loading ? <Spinner style={style.spinner} color={style.spinner.color || 'white'} /> : null} |
|||
</Button> |
|||
); |
|||
} |
|||
|
|||
LoadingButton.propTypes = { |
|||
...Button.propTypes, |
|||
loading: PropTypes.bool.isRequired, |
|||
}; |
|||
|
|||
const styles = StyleSheet.create({ |
|||
button: { marginTop: 20, marginBottom: 30, height: 30 }, |
|||
spinner: { |
|||
transform: [{ scale: 0.5 }], |
|||
color: 'white', |
|||
marginRight: 5, |
|||
}, |
|||
}); |
|||
|
|||
const Forwarded = forwardRef((props, ref) => <LoadingButton {...props} forwardedRef={ref} />); |
|||
|
|||
export default connectStyle('ABP.LoadingButton', styles)(Forwarded); |
|||
@ -0,0 +1,19 @@ |
|||
import React from 'react'; |
|||
import { TouchableOpacity } from 'react-native'; |
|||
import { Icon } from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
|
|||
function MenuIcon({ onPress, iconName = 'menu' }) { |
|||
return ( |
|||
<TouchableOpacity onPress={onPress}> |
|||
<Icon navElement name={iconName} /> |
|||
</TouchableOpacity> |
|||
); |
|||
} |
|||
|
|||
MenuIcon.propTypes = { |
|||
onPress: PropTypes.func.isRequired, |
|||
iconName: PropTypes.string, |
|||
}; |
|||
|
|||
export default MenuIcon; |
|||
@ -0,0 +1,128 @@ |
|||
import i18n from 'i18n-js'; |
|||
import { |
|||
Button, |
|||
connectStyle, |
|||
Content, |
|||
Input, |
|||
InputGroup, |
|||
Label, |
|||
Segment, |
|||
Text, |
|||
} from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { forwardRef, useState } from 'react'; |
|||
import { StyleSheet, View, Alert } from 'react-native'; |
|||
import { getTenant } from '../../api/AccountAPI'; |
|||
import PersistentStorageActions from '../../store/actions/PersistentStorageActions'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import { createTenantSelector } from '../../store/selectors/PersistentStorageSelectors'; |
|||
|
|||
function TenantBox({ style, tenant = {}, setTenant, showTenantSelection, toggleTenantSelection }) { |
|||
const [tenantName, setTenantName] = useState(tenant.name); |
|||
|
|||
const findTenant = () => { |
|||
if (!tenantName) { |
|||
setTenant({}); |
|||
toggleTenantSelection(); |
|||
return; |
|||
} |
|||
|
|||
getTenant(tenantName).then(({ success, ...data }) => { |
|||
if (!success) { |
|||
Alert.alert( |
|||
i18n.t('AbpUi::Error'), |
|||
i18n.t('AbpUiMultiTenancy::GivenTenantIsNotAvailable', { |
|||
0: tenantName, |
|||
}), |
|||
[{ text: i18n.t('AbpUi::Ok') }], |
|||
); |
|||
return; |
|||
} |
|||
setTenant(data); |
|||
toggleTenantSelection(); |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<Segment style={style.container}> |
|||
<View> |
|||
<Text style={style.title}>{i18n.t('AbpUiMultiTenancy::Tenant')}</Text> |
|||
<Text style={style.tenant}> |
|||
{tenant.name ? tenant.name : i18n.t('AbpUiMultiTenancy::NotSelected')} |
|||
</Text> |
|||
</View> |
|||
<Button |
|||
style={{ ...style.switchButton, display: !showTenantSelection ? 'flex' : 'none' }} |
|||
onPress={() => toggleTenantSelection()}> |
|||
<Text style={{ color: '#fff' }}>{i18n.t('AbpUiMultiTenancy::Switch')}</Text> |
|||
</Button> |
|||
</Segment> |
|||
{showTenantSelection ? ( |
|||
<Content px20 style={{ flex: 1 }}> |
|||
<InputGroup abpInputGroup> |
|||
<Label abpLabel>{i18n.t('AbpUiMultiTenancy::Name')}</Label> |
|||
<Input abpInput value={tenantName} onChangeText={setTenantName} /> |
|||
</InputGroup> |
|||
<Text style={style.hint}>{i18n.t('AbpUiMultiTenancy::SwitchTenantHint')}</Text> |
|||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}> |
|||
<Button abpButton light style={style.button} onPress={() => toggleTenantSelection()}> |
|||
<Text>{i18n.t('AbpAccount::Cancel')}</Text> |
|||
</Button> |
|||
<Button abpButton style={style.button} onPress={() => findTenant()}> |
|||
<Text>{i18n.t('AbpAccount::Save')}</Text> |
|||
</Button> |
|||
</View> |
|||
</Content> |
|||
) : null} |
|||
</> |
|||
); |
|||
} |
|||
|
|||
TenantBox.propTypes = { |
|||
style: PropTypes.any.isRequired, |
|||
setTenant: PropTypes.func.isRequired, |
|||
showTenantSelection: PropTypes.bool.isRequired, |
|||
toggleTenantSelection: PropTypes.func.isRequired, |
|||
tenant: PropTypes.object.isRequired, |
|||
}; |
|||
|
|||
const styles = StyleSheet.create({ |
|||
container: { |
|||
paddingHorizontal: 20, |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
height: 70, |
|||
}, |
|||
button: { marginTop: 20, width: '49%' }, |
|||
switchButton: { |
|||
borderTopWidth: 0, |
|||
borderRightWidth: 0, |
|||
borderBottomWidth: 0, |
|||
borderLeftWidth: 0, |
|||
borderRadius: 10, |
|||
backgroundColor: '#38003c', |
|||
height: 35, |
|||
}, |
|||
tenant: { color: '#777' }, |
|||
title: { |
|||
marginRight: 10, |
|||
color: '#777', |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
textTransform: 'uppercase', |
|||
}, |
|||
hint: { color: '#bbb', textAlign: 'left' }, |
|||
}); |
|||
|
|||
const Forwarded = forwardRef((props, ref) => <TenantBox {...props} forwardedRef={ref} />); |
|||
|
|||
export default connectToRedux({ |
|||
component: connectStyle('ABP.TenantBox', styles)(Forwarded), |
|||
dispatchProps: { |
|||
setTenant: PersistentStorageActions.setTenant, |
|||
}, |
|||
stateProps: state => ({ |
|||
tenant: createTenantSelector()(state), |
|||
}), |
|||
}); |
|||
@ -0,0 +1,18 @@ |
|||
import i18n from 'i18n-js'; |
|||
import { connectStyle } from 'native-base'; |
|||
import React, { forwardRef } from 'react'; |
|||
import { Text } from 'react-native'; |
|||
|
|||
const ValidationMessage = ({ children, ...props }) => |
|||
children ? <Text {...props}>{i18n.t(children)}</Text> : null; |
|||
|
|||
const styles = { |
|||
fontSize: 12, |
|||
marginHorizontal: 10, |
|||
marginTop: -5, |
|||
color: '#ed2f2f', |
|||
}; |
|||
|
|||
const Forwarded = forwardRef((props, ref) => <ValidationMessage {...props} forwardedRef={ref} />); |
|||
|
|||
export default connectStyle('ABP.ValidationMessage', styles)(Forwarded); |
|||
@ -0,0 +1,3 @@ |
|||
import React from 'react'; |
|||
|
|||
export const LocalizationContext = React.createContext(); |
|||
@ -0,0 +1,17 @@ |
|||
import React, { forwardRef } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { usePermission } from '../hooks/UsePermission'; |
|||
|
|||
export function withPermission(Component, policyKey) { |
|||
const Forwarded = forwardRef((props, ref) => { |
|||
const isGranted = |
|||
policyKey || props.policyKey ? usePermission(policyKey || props.policyKey) : true; |
|||
return isGranted ? <Component ref={ref} {...props} /> : null; |
|||
}); |
|||
|
|||
Forwarded.propTypes = { |
|||
policyKey: PropTypes.string, |
|||
}; |
|||
|
|||
return Forwarded; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { useEffect, useState } from 'react'; |
|||
import { store } from '../store'; |
|||
import { createGrantedPolicySelector } from '../store/selectors/AppSelectors'; |
|||
|
|||
export function usePermission(key) { |
|||
const [permission, setPermission] = useState(false); |
|||
|
|||
const state = store.getState(); |
|||
const policy = createGrantedPolicySelector(key)(state); |
|||
|
|||
useEffect(() => { |
|||
setPermission(policy); |
|||
}, [policy]); |
|||
|
|||
return permission; |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
import { Toast } from 'native-base'; |
|||
import i18n from 'i18n-js'; |
|||
import api from '../api/API'; |
|||
import PersistentStorageActions from '../store/actions/PersistentStorageActions'; |
|||
import LoadingActions from '../store/actions/LoadingActions'; |
|||
|
|||
export function initAPIInterceptor(store) { |
|||
api.interceptors.request.use( |
|||
async request => { |
|||
const { |
|||
persistentStorage: { token, language, tenant }, |
|||
} = store.getState(); |
|||
|
|||
if (!request.headers.Authorization && token && token.access_token) { |
|||
request.headers.Authorization = `${token.token_type} ${token.access_token}`; |
|||
} |
|||
|
|||
if (!request.headers['Content-Type']) { |
|||
request.headers['Content-Type'] = 'application/json'; |
|||
} |
|||
|
|||
if (!request.headers['Accept-Language'] && language) { |
|||
request.headers['Accept-Language'] = language; |
|||
} |
|||
|
|||
if (!request.headers.__tenant && tenant && tenant.tenantId) { |
|||
request.headers.__tenant = tenant.tenantId; |
|||
} |
|||
|
|||
return request; |
|||
}, |
|||
error => console.error(error), |
|||
); |
|||
|
|||
api.interceptors.response.use( |
|||
response => response, |
|||
error => { |
|||
store.dispatch(LoadingActions.clear()); |
|||
const errorRes = error.response; |
|||
if (errorRes) { |
|||
if (errorRes.headers._abperrorformat && errorRes.status === 401) { |
|||
store.dispatch(PersistentStorageActions.setToken({})); |
|||
} |
|||
|
|||
showError({ error: errorRes.data.error || {}, status: errorRes.status }); |
|||
} else { |
|||
Toast.show({ |
|||
text: 'An unexpected error has occurred', |
|||
buttonText: 'x', |
|||
duration: 10000, |
|||
type: 'danger', |
|||
textStyle: { textAlign: 'center' }, |
|||
}); |
|||
} |
|||
|
|||
return Promise.reject(error); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
function showError({ error = {}, status }) { |
|||
let message = ''; |
|||
let title = i18n.t('AbpAccount::DefaultErrorMessage'); |
|||
|
|||
if (typeof error === 'string') { |
|||
message = error; |
|||
} else if (error.details) { |
|||
message = error.details; |
|||
title = error.message; |
|||
} else if (error.message) { |
|||
message = error.message; |
|||
} else { |
|||
switch (status) { |
|||
case 401: |
|||
title = i18n.t('AbpAccount::DefaultErrorMessage401'); |
|||
message = i18n.t('AbpAccount::DefaultErrorMessage401Detail'); |
|||
break; |
|||
case 403: |
|||
title = i18n.t('AbpAccount::DefaultErrorMessage403'); |
|||
message = i18n.t('AbpAccount::DefaultErrorMessage403Detail'); |
|||
break; |
|||
case 404: |
|||
title = i18n.t('AbpAccount::DefaultErrorMessage404'); |
|||
message = i18n.t('AbpAccount::DefaultErrorMessage404Detail'); |
|||
break; |
|||
case 500: |
|||
title = i18n.t('AbpAccount::500Message'); |
|||
message = i18n.t('AbpAccount::InternalServerErrorMessage'); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
Toast.show({ |
|||
text: `${title}\n${message}`, |
|||
buttonText: 'x', |
|||
duration: 10000, |
|||
type: 'danger', |
|||
textStyle: { textAlign: 'center' }, |
|||
}); |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
import { createStackNavigator } from '@react-navigation/stack'; |
|||
import React from 'react'; |
|||
import { LocalizationContext } from '../contexts/LocalizationContext'; |
|||
import LoginScreen from '../screens/Login/LoginScreen'; |
|||
|
|||
const Stack = createStackNavigator(); |
|||
|
|||
export default function AuthStackNavigator() { |
|||
const { t } = React.useContext(LocalizationContext); |
|||
|
|||
return ( |
|||
<Stack.Navigator initialRouteName="Login"> |
|||
<Stack.Screen |
|||
name="Login" |
|||
component={LoginScreen} |
|||
options={() => ({ |
|||
title: t('AbpAccount::Login'), |
|||
})} |
|||
/> |
|||
</Stack.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import React from 'react'; |
|||
import { createDrawerNavigator } from '@react-navigation/drawer'; |
|||
import HomeStackNavigator from './HomeNavigator'; |
|||
import SettingsStackNavigator from './SettingsNavigator'; |
|||
import UsersStackNavigator from './UsersNavigator'; |
|||
import TenantsStackNavigator from './TenantsNavigator'; |
|||
import DrawerContent from '../components/DrawerContent/DrawerContent'; |
|||
|
|||
const Drawer = createDrawerNavigator(); |
|||
|
|||
export default function DrawerNavigator() { |
|||
return ( |
|||
<Drawer.Navigator initialRouteName="Home" drawerContent={DrawerContent}> |
|||
<Drawer.Screen name="Home" component={HomeStackNavigator} /> |
|||
<Drawer.Screen name="Users" component={UsersStackNavigator} /> |
|||
<Drawer.Screen name="Tenants" component={TenantsStackNavigator} /> |
|||
<Drawer.Screen name="Settings" component={SettingsStackNavigator} /> |
|||
</Drawer.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
import React from 'react'; |
|||
import { createStackNavigator } from '@react-navigation/stack'; |
|||
import HomeScreen from '../screens/Home/HomeScreen'; |
|||
import MenuIcon from '../components/MenuIcon/MenuIcon'; |
|||
import { LocalizationContext } from '../contexts/LocalizationContext'; |
|||
|
|||
const Stack = createStackNavigator(); |
|||
|
|||
export default function HomeStackNavigator() { |
|||
const { t } = React.useContext(LocalizationContext); |
|||
|
|||
return ( |
|||
<Stack.Navigator initialRouteName="Home"> |
|||
<Stack.Screen |
|||
name="Home" |
|||
component={HomeScreen} |
|||
options={({ navigation }) => ({ |
|||
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />, |
|||
title: t('::Menu:Home'), |
|||
})} |
|||
/> |
|||
</Stack.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
import React from 'react'; |
|||
import { createStackNavigator } from '@react-navigation/stack'; |
|||
import i18n from 'i18n-js'; |
|||
import SettingsScreen from '../screens/Settings/SettingsScreen'; |
|||
import ChangePasswordScreen from '../screens/ChangePassword/ChangePasswordScreen'; |
|||
import ManageProfileScreen from '../screens/ManageProfile/ManageProfileScreen'; |
|||
import MenuIcon from '../components/MenuIcon/MenuIcon'; |
|||
import { LocalizationContext } from '../contexts/LocalizationContext'; |
|||
|
|||
const Stack = createStackNavigator(); |
|||
|
|||
export default function SettingsStackNavigator() { |
|||
const { t } = React.useContext(LocalizationContext); |
|||
|
|||
return ( |
|||
<Stack.Navigator initialRouteName="Settings"> |
|||
<Stack.Screen |
|||
name="Settings" |
|||
component={SettingsScreen} |
|||
options={({ navigation }) => ({ |
|||
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />, |
|||
title: t('AbpSettingManagement::Settings'), |
|||
})} |
|||
/> |
|||
<Stack.Screen |
|||
name="ChangePassword" |
|||
component={ChangePasswordScreen} |
|||
options={{ |
|||
title: i18n.t('AbpUi::ChangePassword'), |
|||
}} |
|||
/> |
|||
<Stack.Screen |
|||
name="ManageProfile" |
|||
component={ManageProfileScreen} |
|||
options={{ |
|||
title: i18n.t('AbpAccount::ManageYourProfile'), |
|||
}} |
|||
/> |
|||
</Stack.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import React from 'react'; |
|||
import { createStackNavigator } from '@react-navigation/stack'; |
|||
import TenantsScreen from '../screens/Tenants/TenantsScreen'; |
|||
import CreateUpdateTenantScreen from '../screens/CreateUpdateTenant/CreateUpdateTenantScreen'; |
|||
import MenuIcon from '../components/MenuIcon/MenuIcon'; |
|||
import { LocalizationContext } from '../contexts/LocalizationContext'; |
|||
|
|||
const Stack = createStackNavigator(); |
|||
|
|||
export default function TenantsStackNavigator() { |
|||
const { t } = React.useContext(LocalizationContext); |
|||
|
|||
return ( |
|||
<Stack.Navigator initialRouteName="Tenants"> |
|||
<Stack.Screen |
|||
name="Tenants" |
|||
component={TenantsScreen} |
|||
options={({ navigation }) => ({ |
|||
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />, |
|||
title: t('AbpTenantManagement::Tenants'), |
|||
})} |
|||
/> |
|||
<Stack.Screen |
|||
name="CreateUpdateTenant" |
|||
component={CreateUpdateTenantScreen} |
|||
options={({ route }) => ({ |
|||
title: t( |
|||
route.params?.tenantId ? 'AbpTenantManagement::Edit' : 'AbpTenantManagement::NewTenant', |
|||
), |
|||
})} |
|||
/> |
|||
</Stack.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
import React from 'react'; |
|||
import { createStackNavigator } from '@react-navigation/stack'; |
|||
import UsersScreen from '../screens/Users/UsersScreen'; |
|||
import CreateUpdateUserScreen from '../screens/CreateUpdateUser/CreateUpdateUserScreen'; |
|||
import MenuIcon from '../components/MenuIcon/MenuIcon'; |
|||
import { LocalizationContext } from '../contexts/LocalizationContext'; |
|||
|
|||
const Stack = createStackNavigator(); |
|||
|
|||
export default function UsersStackNavigator() { |
|||
const { t } = React.useContext(LocalizationContext); |
|||
|
|||
return ( |
|||
<Stack.Navigator initialRouteName="Users"> |
|||
<Stack.Screen |
|||
name="Users" |
|||
component={UsersScreen} |
|||
options={({ navigation }) => ({ |
|||
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />, |
|||
title: t('AbpIdentity::Users'), |
|||
})} |
|||
/> |
|||
<Stack.Screen |
|||
name="CreateUpdateUser" |
|||
component={CreateUpdateUserScreen} |
|||
options={({ route }) => ({ |
|||
title: t(route.params?.userId ? 'AbpIdentity::Edit' : 'AbpIdentity::NewUser'), |
|||
})} |
|||
/> |
|||
</Stack.Navigator> |
|||
); |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
import { Formik } from 'formik'; |
|||
import i18n from 'i18n-js'; |
|||
import { Container, Content, Form, Input, InputGroup, Item, Icon, Label } from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { useRef, useState } from 'react'; |
|||
import * as Yup from 'yup'; |
|||
import FormButtons from '../../components/FormButtons/FormButtons'; |
|||
import ValidationMessage from '../../components/ValidationMessage/ValidationMessage'; |
|||
|
|||
const ValidationSchema = Yup.object().shape({ |
|||
currentPassword: Yup.string().required('AbpAccount::ThisFieldIsRequired.'), |
|||
newPassword: Yup.string().required('AbpAccount::ThisFieldIsRequired.'), |
|||
}); |
|||
|
|||
function ChangePasswordForm({ submit, cancel }) { |
|||
const [showCurrentPassword, setShowCurrentPassword] = useState(false); |
|||
const [showNewPassword, setShowNewPassword] = useState(false); |
|||
|
|||
const currentPasswordRef = useRef(); |
|||
const newPasswordRef = useRef(); |
|||
|
|||
const onSubmit = values => { |
|||
submit({ |
|||
...values, |
|||
newPasswordConfirm: values.newPassword, |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Formik |
|||
enableReinitialize |
|||
validationSchema={ValidationSchema} |
|||
initialValues={{ |
|||
currentPassword: '', |
|||
newPassword: '', |
|||
}} |
|||
onSubmit={values => onSubmit(values)}> |
|||
{({ handleChange, handleBlur, handleSubmit, values, errors, isValid }) => ( |
|||
<> |
|||
<Container> |
|||
<Content px20> |
|||
<Form> |
|||
<InputGroup abpInputGroup> |
|||
<Label abpLabel>{i18n.t('AbpIdentity::DisplayName:CurrentPassword')}</Label> |
|||
<Item abpInput> |
|||
<Input |
|||
ref={currentPasswordRef} |
|||
onSubmitEditing={() => newPasswordRef.current._root.focus()} |
|||
returnKeyType="next" |
|||
onChangeText={handleChange('currentPassword')} |
|||
onBlur={handleBlur('currentPassword')} |
|||
value={values.currentPassword} |
|||
textContentType="password" |
|||
secureTextEntry={!showCurrentPassword} |
|||
/> |
|||
<Icon |
|||
active |
|||
name={showCurrentPassword ? 'eye-off' : 'eye'} |
|||
onPress={() => setShowCurrentPassword(!showCurrentPassword)} |
|||
/> |
|||
</Item> |
|||
</InputGroup> |
|||
<ValidationMessage>{errors.currentPassword}</ValidationMessage> |
|||
<InputGroup abpInputGroup> |
|||
<Label abpLabel>{i18n.t('AbpIdentity::DisplayName:NewPassword')}</Label> |
|||
<Item abpInput> |
|||
<Input |
|||
ref={newPasswordRef} |
|||
returnKeyType="done" |
|||
onSubmitEditing={handleSubmit} |
|||
onChangeText={handleChange('newPassword')} |
|||
onBlur={handleBlur('newPassword')} |
|||
value={values.newPassword} |
|||
textContentType="newPassword" |
|||
secureTextEntry={!showNewPassword} |
|||
/> |
|||
<Icon |
|||
name={showNewPassword ? 'eye-off' : 'eye'} |
|||
onPress={() => setShowNewPassword(!showNewPassword)} |
|||
/> |
|||
</Item> |
|||
</InputGroup> |
|||
<ValidationMessage>{errors.newPassword}</ValidationMessage> |
|||
</Form> |
|||
</Content> |
|||
</Container> |
|||
<FormButtons submit={handleSubmit} cancel={cancel} isSubmitDisabled={!isValid} /> |
|||
</> |
|||
)} |
|||
</Formik> |
|||
); |
|||
} |
|||
|
|||
ChangePasswordForm.propTypes = { |
|||
submit: PropTypes.func.isRequired, |
|||
cancel: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default ChangePasswordForm; |
|||
@ -0,0 +1,33 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { changePassword } from '../../api/IdentityAPI'; |
|||
import LoadingActions from '../../store/actions/LoadingActions'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import ChangePasswordForm from './ChangePasswordForm'; |
|||
|
|||
function ChangePasswordScreen({ navigation, startLoading, stopLoading }) { |
|||
const submit = data => { |
|||
startLoading({ key: 'changePassword' }); |
|||
|
|||
changePassword(data) |
|||
.then(() => { |
|||
navigation.goBack(); |
|||
}) |
|||
.finally(() => stopLoading({ key: 'changePassword' })); |
|||
}; |
|||
|
|||
return <ChangePasswordForm submit={submit} cancel={() => navigation.goBack()} />; |
|||
} |
|||
|
|||
ChangePasswordScreen.propTypes = { |
|||
startLoading: PropTypes.func.isRequired, |
|||
stopLoading: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default connectToRedux({ |
|||
component: ChangePasswordScreen, |
|||
dispatchProps: { |
|||
startLoading: LoadingActions.start, |
|||
stopLoading: LoadingActions.stop, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,84 @@ |
|||
import { Formik } from 'formik'; |
|||
import i18n from 'i18n-js'; |
|||
import { Container, Content, Input, InputGroup, Label } from 'native-base'; |
|||
import PropTypes from 'prop-types'; |
|||
import React, { useRef } from 'react'; |
|||
import { StyleSheet } from 'react-native'; |
|||
import * as Yup from 'yup'; |
|||
import FormButtons from '../../components/FormButtons/FormButtons'; |
|||
import ValidationMessage from '../../components/ValidationMessage/ValidationMessage'; |
|||
import { usePermission } from '../../hooks/UsePermission'; |
|||
|
|||
const validations = { |
|||
name: Yup.string().required('AbpAccount::ThisFieldIsRequired.'), |
|||
}; |
|||
|
|||
function CreateUpdateTenantForm({ editingTenant = {}, submit, remove }) { |
|||
const tenantNameRef = useRef(); |
|||
|
|||
const hasRemovePermission = usePermission('AbpTenantManagement.Tenants.Delete'); |
|||
|
|||
const onSubmit = values => { |
|||
submit({ |
|||
...editingTenant, |
|||
...values, |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<Formik |
|||
enableReinitialize |
|||
validationSchema={Yup.object().shape({ |
|||
...validations, |
|||
})} |
|||
initialValues={{ |
|||
lockoutEnabled: false, |
|||
twoFactorEnabled: false, |
|||
...editingTenant, |
|||
}} |
|||
onSubmit={values => onSubmit(values)}> |
|||
{({ handleChange, handleBlur, handleSubmit, values, errors, isValid }) => ( |
|||
<> |
|||
<Container style={styles.container}> |
|||
<Content px20> |
|||
<InputGroup abpInputGroup> |
|||
<Label abpLabel>{i18n.t('AbpTenantManagement::TenantName')}</Label> |
|||
<Input |
|||
abpInput |
|||
ref={tenantNameRef} |
|||
onChangeText={handleChange('name')} |
|||
onBlur={handleBlur('name')} |
|||
value={values.name} |
|||
/> |
|||
</InputGroup> |
|||
<ValidationMessage>{errors.name}</ValidationMessage> |
|||
</Content> |
|||
</Container> |
|||
<FormButtons |
|||
submit={handleSubmit} |
|||
remove={remove} |
|||
removeMessage={i18n.t('AbpTenantManagement::TenantDeletionConfirmationMessage', { |
|||
0: editingTenant.name, |
|||
})} |
|||
isSubmitDisabled={!isValid} |
|||
isShowRemove={!!editingTenant.id && hasRemovePermission} |
|||
/> |
|||
</> |
|||
)} |
|||
</Formik> |
|||
); |
|||
} |
|||
|
|||
CreateUpdateTenantForm.propTypes = { |
|||
editingTenant: PropTypes.object, |
|||
submit: PropTypes.func.isRequired, |
|||
remove: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
const styles = StyleSheet.create({ |
|||
container: { |
|||
marginBottom: 50, |
|||
}, |
|||
}); |
|||
|
|||
export default CreateUpdateTenantForm; |
|||
@ -0,0 +1,77 @@ |
|||
import PropTypes from 'prop-types'; |
|||
import React, { useState, useCallback } from 'react'; |
|||
import { useFocusEffect } from '@react-navigation/native'; |
|||
import { |
|||
createTenant, |
|||
getTenantById, |
|||
removeTenant, |
|||
updateTenant, |
|||
} from '../../api/TenantManagementAPI'; |
|||
import LoadingActions from '../../store/actions/LoadingActions'; |
|||
import { createLoadingSelector } from '../../store/selectors/LoadingSelectors'; |
|||
import { connectToRedux } from '../../utils/ReduxConnect'; |
|||
import CreateUpdateTenantForm from './CreateUpdateTenantForm'; |
|||
|
|||
function CreateUpdateTenantScreen({ navigation, route, startLoading, stopLoading }) { |
|||
const [tenant, setTenant] = useState(); |
|||
const tenantId = route.params?.tenantId; |
|||
|
|||
const remove = () => { |
|||
startLoading({ key: 'removeTenant' }); |
|||
removeTenant(tenantId) |
|||
.then(() => navigation.goBack()) |
|||
.finally(() => stopLoading({ key: 'removeTenant' })); |
|||
}; |
|||
|
|||
useFocusEffect( |
|||
useCallback(() => { |
|||
if (tenantId) { |
|||
getTenantById(tenantId).then((data = {}) => setTenant(data)); |
|||
} |
|||
}, []), |
|||
); |
|||
|
|||
const submit = data => { |
|||
startLoading({ key: 'saveTenant' }); |
|||
let request; |
|||
if (data.id) { |
|||
request = updateTenant(data, tenantId); |
|||
} else { |
|||
request = createTenant(data); |
|||
} |
|||
|
|||
request |
|||
.then(() => { |
|||
navigation.goBack(); |
|||
}) |
|||
.finally(() => stopLoading({ key: 'saveTenant' })); |
|||
}; |
|||
|
|||
const renderForm = () => ( |
|||
<CreateUpdateTenantForm editingTenant={tenant} submit={submit} remove={remove} /> |
|||
); |
|||
|
|||
if (tenantId && tenant) { |
|||
return renderForm(); |
|||
} |
|||
|
|||
if (!tenantId) { |
|||
return renderForm(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
CreateUpdateTenantScreen.propTypes = { |
|||
startLoading: PropTypes.func.isRequired, |
|||
stopLoading: PropTypes.func.isRequired, |
|||
}; |
|||
|
|||
export default connectToRedux({ |
|||
component: CreateUpdateTenantScreen, |
|||
stateProps: state => ({ loading: createLoadingSelector()(state) }), |
|||
dispatchProps: { |
|||
startLoading: LoadingActions.start, |
|||
stopLoading: LoadingActions.stop, |
|||
}, |
|||
}); |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue