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 |
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 |
public abstract class TenantCreateOrUpdateDtoBase |
||||
{ |
{ |
||||
|
[Required] |
||||
|
[StringLength(TenantConsts.MaxNameLength)] |
||||
public string Name { get; set; } |
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