forked from mirrors/linphone-iphone
contacts: fix contact creation and tests
This commit is contained in:
parent
de2066f71c
commit
f4d25742c4
14 changed files with 62 additions and 126 deletions
|
|
@ -24,8 +24,8 @@
|
|||
|
||||
typedef enum _ContactSections {
|
||||
ContactSections_None = 0, // first section is empty because we cannot set header for first section
|
||||
ContactSections_First_Name,
|
||||
ContactSections_Last_Name,
|
||||
ContactSections_FirstName,
|
||||
ContactSections_LastName,
|
||||
ContactSections_Sip,
|
||||
ContactSections_Number,
|
||||
ContactSections_Email,
|
||||
|
|
@ -36,7 +36,6 @@ typedef enum _ContactSections {
|
|||
@private
|
||||
NSMutableArray *dataCache;
|
||||
NSMutableArray *labelArray;
|
||||
NSIndexPath *editingIndexPath;
|
||||
}
|
||||
|
||||
@property(nonatomic, assign, setter=setContact:) ABRecordRef contact;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
#pragma mark - Lifecycle Functions
|
||||
|
||||
- (void)initContactDetailsTableViewController {
|
||||
INIT_WITH_COMMON_C {
|
||||
dataCache = [[NSMutableArray alloc] init];
|
||||
|
||||
// pre-fill the data-cache with empty arrays
|
||||
|
|
@ -66,22 +66,6 @@
|
|||
[NSString stringWithString:(NSString *)kABPersonPhoneMobileLabel],
|
||||
[NSString stringWithString:(NSString *)kABPersonPhoneIPhoneLabel],
|
||||
[NSString stringWithString:(NSString *)kABPersonPhoneMainLabel], nil];
|
||||
editingIndexPath = nil;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self initContactDetailsTableViewController];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)decoder {
|
||||
self = [super initWithCoder:decoder];
|
||||
if (self) {
|
||||
[self initContactDetailsTableViewController];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
|
@ -93,10 +77,6 @@
|
|||
|
||||
#pragma mark -
|
||||
|
||||
- (void)updateModification {
|
||||
[contactDetailsDelegate onModification:nil];
|
||||
}
|
||||
|
||||
- (NSMutableArray *)getSectionData:(NSInteger)section {
|
||||
if (section == ContactSections_Number) {
|
||||
return [dataCache objectAtIndex:0];
|
||||
|
|
@ -114,9 +94,9 @@
|
|||
|
||||
- (ABPropertyID)propertyIDForSection:(ContactSections)section {
|
||||
switch (section) {
|
||||
case ContactSections_First_Name:
|
||||
case ContactSections_FirstName:
|
||||
return kABPersonFirstNameProperty;
|
||||
case ContactSections_Last_Name:
|
||||
case ContactSections_LastName:
|
||||
return kABPersonLastNameProperty;
|
||||
case ContactSections_Sip:
|
||||
return kABPersonInstantMessageProperty;
|
||||
|
|
@ -482,7 +462,7 @@
|
|||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
if (section == ContactSections_First_Name || section == ContactSections_Last_Name) {
|
||||
if (section == ContactSections_FirstName || section == ContactSections_LastName) {
|
||||
return (self.tableView.isEditing) ? 1 : 0 /*no first and last name when not editting */;
|
||||
} else {
|
||||
return [[self getSectionData:section] count];
|
||||
|
|
@ -496,19 +476,20 @@
|
|||
cell = [[UIContactDetailsCell alloc] initWithIdentifier:kCellId];
|
||||
[cell.editTextfield setDelegate:self];
|
||||
}
|
||||
cell.indexPath = indexPath;
|
||||
|
||||
NSMutableArray *sectionDict = [self getSectionData:[indexPath section]];
|
||||
Entry *entry = [sectionDict objectAtIndex:[indexPath row]];
|
||||
|
||||
NSString *value = @"";
|
||||
[cell hideDeleteButton:NO];
|
||||
if (indexPath.section == ContactSections_First_Name) {
|
||||
if (indexPath.section == ContactSections_FirstName) {
|
||||
value = (NSString *)CFBridgingRelease(
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_First_Name]));
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_FirstName]));
|
||||
[cell hideDeleteButton:YES];
|
||||
} else if (indexPath.section == ContactSections_Last_Name) {
|
||||
} else if (indexPath.section == ContactSections_LastName) {
|
||||
value = (NSString *)CFBridgingRelease(
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_Last_Name]));
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_LastName]));
|
||||
[cell hideDeleteButton:YES];
|
||||
} else if ([indexPath section] == ContactSections_Number) {
|
||||
ABMultiValueRef lMap = ABRecordCopyValue(contact, kABPersonPhoneProperty);
|
||||
|
|
@ -565,30 +546,6 @@
|
|||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:NO];
|
||||
NSMutableArray *sectionDict = [self getSectionData:[indexPath section]];
|
||||
Entry *entry = [sectionDict objectAtIndex:[indexPath row]];
|
||||
if ([self isEditing]) {
|
||||
NSString *key = nil;
|
||||
ABPropertyID property = [self propertyIDForSection:(ContactSections)indexPath.section];
|
||||
|
||||
if (property != kABInvalidPropertyType && property != kABPersonFirstNameProperty &&
|
||||
property != kABPersonLastNameProperty) {
|
||||
ABMultiValueRef lMap = ABRecordCopyValue(contact, property);
|
||||
NSInteger index = ABMultiValueGetIndexForIdentifier(lMap, [entry identifier]);
|
||||
NSString *labelRef = CFBridgingRelease(ABMultiValueCopyLabelAtIndex(lMap, index));
|
||||
if (labelRef != NULL) {
|
||||
key = (NSString *)(labelRef);
|
||||
}
|
||||
CFRelease(lMap);
|
||||
}
|
||||
if (key != nil) {
|
||||
editingIndexPath = indexPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
|
|
@ -648,10 +605,10 @@
|
|||
NSString *text = nil;
|
||||
BOOL canAddEntry = self.tableView.isEditing;
|
||||
NSString *addEntryName = nil;
|
||||
if (section == ContactSections_First_Name && self.tableView.isEditing) {
|
||||
if (section == ContactSections_FirstName && self.tableView.isEditing) {
|
||||
text = NSLocalizedString(@"First name", nil);
|
||||
canAddEntry = NO;
|
||||
} else if (section == ContactSections_Last_Name && self.tableView.isEditing) {
|
||||
} else if (section == ContactSections_LastName && self.tableView.isEditing) {
|
||||
text = NSLocalizedString(@"Last name", nil);
|
||||
canAddEntry = NO;
|
||||
} else if ([self getSectionData:section].count > 0 || self.tableView.isEditing) {
|
||||
|
|
@ -711,40 +668,13 @@
|
|||
forRowAtIndexPath:indexPath];
|
||||
}
|
||||
|
||||
#pragma mark - ContactDetailsLabelDelegate Functions
|
||||
|
||||
- (void)changeContactDetailsLabel:(NSString *)value {
|
||||
if (value != nil) {
|
||||
NSInteger section = editingIndexPath.section;
|
||||
NSMutableArray *sectionDict = [self getSectionData:section];
|
||||
ABPropertyID property = [self propertyIDForSection:(int)section];
|
||||
Entry *entry = [sectionDict objectAtIndex:editingIndexPath.row];
|
||||
|
||||
if (property != kABInvalidPropertyType) {
|
||||
ABMultiValueRef lcMap = ABRecordCopyValue(contact, kABPersonPhoneProperty);
|
||||
ABMutableMultiValueRef lMap = ABMultiValueCreateMutableCopy(lcMap);
|
||||
CFRelease(lcMap);
|
||||
NSInteger index = ABMultiValueGetIndexForIdentifier(lMap, [entry identifier]);
|
||||
ABMultiValueReplaceLabelAtIndex(lMap, (__bridge CFStringRef)(value), index);
|
||||
ABRecordSetValue(contact, kABPersonPhoneProperty, lMap, nil);
|
||||
CFRelease(lMap);
|
||||
}
|
||||
|
||||
[self.tableView beginUpdates];
|
||||
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:editingIndexPath] withRowAnimation:FALSE];
|
||||
[self.tableView reloadSectionIndexTitles];
|
||||
[self.tableView endUpdates];
|
||||
}
|
||||
editingIndexPath = nil;
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate Functions
|
||||
|
||||
- (BOOL)textField:(UITextField *)textField
|
||||
shouldChangeCharactersInRange:(NSRange)range
|
||||
replacementString:(NSString *)string {
|
||||
if (contactDetailsDelegate != nil) {
|
||||
[self performSelector:@selector(updateModification) withObject:nil afterDelay:0];
|
||||
[contactDetailsDelegate onModification:nil];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
|
@ -756,26 +686,25 @@
|
|||
|
||||
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
|
||||
UIView *view = [textField superview];
|
||||
|
||||
// Find TableViewCell
|
||||
while (view != nil && ![view isKindOfClass:[UIContactDetailsCell class]])
|
||||
view = [view superview];
|
||||
if (view != nil) {
|
||||
UIContactDetailsCell *cell = (UIContactDetailsCell *)view;
|
||||
NSIndexPath *path = [self.tableView indexPathForCell:cell];
|
||||
NSMutableArray *sectionDict = [self getSectionData:[path section]];
|
||||
Entry *entry = [sectionDict objectAtIndex:[path row]];
|
||||
// we cannot use indexPathForCell method here because if the cell is not visible anymore,
|
||||
// it will return nil..
|
||||
NSIndexPath *path = cell.indexPath; // [self.tableView indexPathForCell:cell];
|
||||
ContactSections sect = (ContactSections)[path section];
|
||||
|
||||
ABPropertyID property = [self propertyIDForSection:sect];
|
||||
NSString *value = [textField text];
|
||||
|
||||
NSMutableArray *sectionDict = [self getSectionData:[path section]];
|
||||
Entry *entry = [sectionDict objectAtIndex:[path row]];
|
||||
|
||||
switch (sect) {
|
||||
case ContactSections_First_Name:
|
||||
case ContactSections_Last_Name: {
|
||||
// [cell.detailTextLabel setText:[textField text]];
|
||||
case ContactSections_FirstName:
|
||||
case ContactSections_LastName: {
|
||||
CFErrorRef error = NULL;
|
||||
ABRecordSetValue(contact, property, (__bridge CFTypeRef)([textField text]), (CFErrorRef *)&error);
|
||||
ABRecordSetValue(contact, property, (__bridge CFTypeRef)value, (CFErrorRef *)&error);
|
||||
if (error != NULL) {
|
||||
LOGE(@"Error when saving property %i in contact %p: Fail(%@)", property, contact,
|
||||
[(__bridge NSError *)error localizedDescription]);
|
||||
|
|
@ -805,15 +734,15 @@
|
|||
LOGE(@"Not valid UIEditableTableViewCell");
|
||||
}
|
||||
if (contactDetailsDelegate != nil) {
|
||||
[self performSelector:@selector(updateModification) withObject:nil afterDelay:0];
|
||||
[contactDetailsDelegate onModification:nil];
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
- (BOOL)isValid {
|
||||
NSString *firstName = (NSString *)CFBridgingRelease(
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_First_Name]));
|
||||
NSString *lastName = (NSString *)CFBridgingRelease(
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_Last_Name]));
|
||||
ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_FirstName]));
|
||||
NSString *lastName =
|
||||
(NSString *)CFBridgingRelease(ABRecordCopyValue(contact, [self propertyIDForSection:ContactSections_LastName]));
|
||||
return firstName.length > 0 || lastName.length > 0;
|
||||
}
|
||||
|
||||
|
|
@ -829,8 +758,8 @@
|
|||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
|
||||
if (section == 0 || (!self.tableView.isEditing &&
|
||||
(section == ContactSections_First_Name || section == ContactSections_Last_Name))) {
|
||||
if (section == 0 ||
|
||||
(!self.tableView.isEditing && (section == ContactSections_FirstName || section == ContactSections_LastName))) {
|
||||
return 1e-5;
|
||||
}
|
||||
return [self tableView:tableView viewForHeaderInSection:section].frame.size.height;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
@implementation UIAssistantTextField
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
self.delegate = self;
|
||||
return self;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ static NSString *const kDisappearAnimation = @"disappear";
|
|||
|
||||
@implementation UIBouncingView
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(settingsUpdate:)
|
||||
name:kLinphoneSettingsUpdate
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
#pragma mark - Lifecycle Functions
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
[self addTarget:self action:@selector(touchUp:) forControlEvents:UIControlEventTouchUpInside];
|
||||
return self;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@
|
|||
|
||||
@interface UIContactDetailsCell : UITableViewCell
|
||||
|
||||
// this is broken design... but we need this to know which cell was modified
|
||||
// last... must be totally revamped
|
||||
@property(strong) NSIndexPath *indexPath;
|
||||
|
||||
@property(weak, nonatomic) IBOutlet UIView *defaultView;
|
||||
@property(weak, nonatomic) IBOutlet UILabel *addressLabel;
|
||||
@property(weak, nonatomic) IBOutlet UITextField *editTextfield;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
@implementation UIIconButton
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
[super setImage:[self imageForState:UIControlStateNormal]
|
||||
forState:(UIControlStateHighlighted | UIControlStateSelected)];
|
||||
[super setImage:[self imageForState:UIControlStateDisabled]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
UIView *borderView;
|
||||
}
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
borderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
|
||||
borderView.layer.borderWidth = 10;
|
||||
borderView.layer.borderColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"color_A.png"]].CGColor;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ static void audioRouteChangeListenerCallback(void *inUserData, // 1
|
|||
[button update];
|
||||
}
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
AudioSessionInitialize(NULL, NULL, NULL, NULL);
|
||||
OSStatus lStatus = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange,
|
||||
audioRouteChangeListenerCallback, (__bridge void *)(self));
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
@synthesize waitView;
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
last_update_state = FALSE;
|
||||
return self;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@
|
|||
|
||||
@implementation UINavigationBarEx
|
||||
|
||||
INIT_WITH_COMMON {
|
||||
INIT_WITH_COMMON_CF {
|
||||
[self setTintColor:[LINPHONE_MAIN_COLOR adjustHue:5.0f / 180.0f saturation:0.0f brightness:0.0f alpha:0.0f]];
|
||||
return self;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ typedef enum {
|
|||
|
||||
/* Use that macro when you want to invoke a custom initialisation method on your class,
|
||||
whatever is using it (xib, source code, etc., tableview cell) */
|
||||
#define INIT_WITH_COMMON \
|
||||
#define INIT_WITH_COMMON_C \
|
||||
-(instancetype)init { \
|
||||
self = [super init]; \
|
||||
[self commonInit]; \
|
||||
|
|
@ -105,9 +105,12 @@ typedef enum {
|
|||
[self commonInit]; \
|
||||
return self; \
|
||||
} \
|
||||
-(instancetype)commonInit
|
||||
|
||||
#define INIT_WITH_COMMON_CF \
|
||||
-(instancetype)initWithFrame : (CGRect)frame { \
|
||||
self = [super initWithFrame:frame]; \
|
||||
[self commonInit]; \
|
||||
return self; \
|
||||
} \
|
||||
-(instancetype)commonInit
|
||||
INIT_WITH_COMMON_C
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@
|
|||
traits:UIAccessibilityTraitButton | UIAccessibilityTraitNotEnabled |
|
||||
UIAccessibilityTraitSelected];
|
||||
|
||||
[self setText:firstName forIndex:0 inSection:ContactSections_First_Name];
|
||||
[self setText:firstName forIndex:0 inSection:ContactSections_FirstName];
|
||||
|
||||
// entering text should enable the "edit" button
|
||||
[tester waitForViewWithAccessibilityLabel:@"Edit" traits:UIAccessibilityTraitButton | UIAccessibilityTraitSelected];
|
||||
|
||||
if (lastName) {
|
||||
[self setText:lastName forIndex:0 inSection:ContactSections_Last_Name];
|
||||
[self setText:lastName forIndex:0 inSection:ContactSections_LastName];
|
||||
}
|
||||
|
||||
if (phone) {
|
||||
|
|
@ -70,17 +70,18 @@
|
|||
- (void)tapCellForRowAtIndexPath:(NSInteger)idx inSection:(NSInteger)section atX:(CGFloat)x {
|
||||
UITableView *tv = [self findTableView:@"Contact table"];
|
||||
NSIndexPath *path = [NSIndexPath indexPathForRow:idx inSection:section];
|
||||
UITableViewCell *last =
|
||||
UITableViewCell *cell =
|
||||
[tester waitForCellAtIndexPath:path inTableViewWithAccessibilityIdentifier:@"Contact table"];
|
||||
XCTAssertNotNil(last);
|
||||
XCTAssertNotNil(cell);
|
||||
|
||||
CGRect cellFrame = [last.contentView convertRect:last.contentView.frame toView:tv];
|
||||
[tv tapAtPoint:CGPointMake(x > 0 ? x : cellFrame.size.width + x, cellFrame.origin.y + cellFrame.size.height / 2.)];
|
||||
CGRect cellFrame = [cell.contentView convertRect:cell.contentView.frame toView:tv];
|
||||
[tv tapAtPoint:CGPointMake(x > 0 ? x : tv.superview.frame.size.width + x,
|
||||
cellFrame.origin.y + cellFrame.size.height / 2.)];
|
||||
[tester waitForAnimationsToFinish];
|
||||
}
|
||||
|
||||
- (void)tapRemoveButtonForRowAtIndexPath:(NSInteger)idx inSection:(NSInteger)section {
|
||||
[self tapCellForRowAtIndexPath:idx inSection:section atX:-10];
|
||||
[self tapCellForRowAtIndexPath:idx inSection:section atX:-7];
|
||||
}
|
||||
|
||||
- (void)addEntries:(NSArray *)numbers inSection:(NSInteger)section {
|
||||
|
|
@ -100,12 +101,12 @@
|
|||
}
|
||||
|
||||
- (void)deleteContactEntryForRowAtIndexPath:(NSInteger)idx inSection:(NSInteger)section {
|
||||
if ([tester tryFindingTappableViewWithAccessibilityLabel:@"Delete" error:nil]) {
|
||||
[tester tapViewWithAccessibilityLabel:@"Delete"];
|
||||
} else {
|
||||
// hack: Travis seems to be unable to click on delete for what ever reason
|
||||
// if ([tester tryFindingTappableViewWithAccessibilityLabel:@"Delete" error:nil]) {
|
||||
// [tester tapViewWithAccessibilityLabel:@"Delete"];
|
||||
// } else {
|
||||
// hack: Travis seems to be unable to click on delete for what ever reason
|
||||
[self tapRemoveButtonForRowAtIndexPath:idx inSection:section];
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
#pragma mark - Tests
|
||||
|
|
@ -129,7 +130,6 @@
|
|||
[tester tapViewWithAccessibilityLabel:fullName traits:UIAccessibilityTraitStaticText];
|
||||
|
||||
[tester tapViewWithAccessibilityLabel:@"Edit"];
|
||||
[tester scrollViewWithAccessibilityIdentifier:@"Contact table" byFractionOfSizeHorizontal:0 vertical:-0.9];
|
||||
|
||||
[tester tapViewWithAccessibilityLabel:@"Delete"];
|
||||
[tester tapViewWithAccessibilityLabel:@"DELETE"];
|
||||
|
|
@ -148,11 +148,11 @@
|
|||
[tester tapViewWithAccessibilityLabel:@"Edit"];
|
||||
// remove all numbers
|
||||
for (NSInteger i = 0; i < phones.count; i++) {
|
||||
[self tapRemoveButtonForRowAtIndexPath:0 inSection:ContactSections_Number];
|
||||
[self deleteContactEntryForRowAtIndexPath:0 inSection:ContactSections_Number];
|
||||
}
|
||||
// remove all SIPs
|
||||
for (NSInteger i = 0; i < SIPs.count; i++) {
|
||||
[self tapRemoveButtonForRowAtIndexPath:0 inSection:ContactSections_Sip];
|
||||
[self deleteContactEntryForRowAtIndexPath:0 inSection:ContactSections_Sip];
|
||||
}
|
||||
[tester tapViewWithAccessibilityLabel:@"Edit"];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#import <KIF/UIApplication-KIFAdditions.h>
|
||||
|
||||
#import "Utils.h"
|
||||
#import "Log.h"
|
||||
|
||||
@interface LinphoneTestCase : KIFTestCase
|
||||
@property BOOL invalidAccountSet;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue