Zend Certified Engineer

phpguru.org

Free PHP, Javascript and C# code

Try P3P Create! to create your P3P policy online!

MySQL Table Editor

A PHP 4 & 5 library to allow easy and user-friendly editing of MySQL tables. Note this is a library and not an application. It will be of no use unless you do some coding (not exactly a great deal though - see below).

Note: This is not meant to be an alternative or replacement to phpMyAdmin. This is something you can provide to users to manage tables without them happily destroying your database.

You can get the code here.

Features

  • Now (as of 1.0.5) works with both PHP4 and PHP5
  • Easy setup. Give it a connection resource and a table name and it will work most things out by itself.
  • Search functionality (albeit basic MySQL LIKE). Can be disabled (default), and limited to certain fields only. Advanced Search (version 1.0.6) can be used to search specific fields only, or with various operators (LIKE, =, >, >=, <, <=).
  • View/Add/Copy/Edit/Delete functionality, any or all of which can be disabled.
  • CSV download for selected rows, current page or entire table. Can be disabled. (Version 1.0.6).
  • Cusomisable header/footers.
  • Common functionality such as date/time/timestamp catered for in dropdowns (see add/edit pages in demo). Also supports adding new such functionality.
  • Fields can be hidden from display, editing or both.
  • Ordering by any column supported.
  • Default values for fields can be set for when adding rows.
  • Callbacks can be assigned to fields for pre-display manipulation (Eg. cutting long strings down).
  • Callbacks can be assigned to any/all of add/edit/delete actions (Eg. for logging user actions).
  • Can assign predetermined values to fields (Eg. showing "nicer" values for foreign keys). These values can be supplied as an array, or from the results of an SQL query.
  • Multiple deletes at once support.
  • Paging support. Number of rows per page is configurable.
  • Data filter support, to allow only showing of certain rows.
  • Data validation callback to allow custom checking and/or manipulation of data before being added/updated.
  • Custom field types: password - provides a familiar two input process for setting passwords (ie. password and password confirmation input boxes), email - automatically checks the data matches an email address format, date, time and datetime - automatically sets the default value to the appropriate current value, and adds a "now" link next to the input for setting the current value during add/copy/edit.
  • Required field support, to ensure certain fields are filled in during add/edit actions.
  • Caching of table data structure (so it's not worked out on every page refresh).

Demo

There's a demo here, and the second code listing below is the script that drives it.

Example Usage

The following example is used in my admin area to allow administration of comments:

<?php
/**
* Edits news table
*/
require_once('TableEditor.php');

$editor = new TableEditor($database->connection, 'comments');

$editor->noDisplay('cm_parentid');
$editor->noEdit('cm_parentid');

$editor->setDisplayNames(array('cm_id'       => 'ID',
                              
'cm_neid'     => 'Article',
                              
'cm_datetime' => 'Date Added',
                              
'cm_author'   => 'Author',
                              
'cm_text'     => 'Text',
                              
'cm_email'    => 'Email'));

$editor->setDefaultOrderby('cm_datetime', 0);
$editor->setConfig('searchableFields', array('cm_author', 'cm_text', 'cm_email'));

$editor->setInputType('cm_neid', 'select');
$editor->setValuesFromQuery('cm_neid', "SELECT ne_id, ne_title FROM news");

$editor->addDisplayFilter('cm_text', create_function('$v', 'return str_curtail($v, 100);'));

$editor->display();
?>

This is the demo script:

<?php

/**
* Demo of TableEditor class. Uses the following table:
*
* CREATE TABLE `TableEditorDemo` (
*   `te_id` int(10) unsigned NOT NULL auto_increment,
*   `te_name` varchar(32) NOT NULL default '',
*   `te_password` varchar(32) NOT NULL default '',
*   `te_email` varchar(32) NOT NULL default '',
*   `te_datetime` datetime NOT NULL default '0000-00-00 00:00:00',
*   `te_age` tinyint(3) unsigned NOT NULL default '0',
*   `te_live` enum('LIVE','NOT LIVE') default NULL,
*   `te_desc` mediumtext NOT NULL,
*   PRIMARY KEY  (`te_id`)
* ) TYPE=MyISAM;
*/

require_once('TableEditor.php');

$editor = new TableEditor($conn, 'TableEditorDemo');
    
// $editor->setConfig('allowView', false);
// $editor->setConfig('allowAdd', false);
// $editor->setConfig('allowEdit', false);
// $editor->setConfig('allowCopy', false);
// $editor->setConfig('allowDelete', false);

$editor->setConfig('perPage', 15);

$editor->setDisplayNames(array('te_id'       => 'ID',
                               
'te_name'     => 'Name',
                               
'te_password' => 'Password',
                               
'te_email'    => 'Email',
                               
'te_datetime' => 'Date Added',
                               
'te_age'      => 'Age',
                               
'te_live'     => 'Live',
                               
'te_desc'     => 'Description'));

// $editor->noDisplay('te_password');
$editor->noEdit('te_live');

$editor->setInputType('te_password', 'password');
$editor->setInputType('te_email', 'email');

$editor->setSearchableFields('te_name', 'te_age', 'te_id', 'te_desc', 'te_live');
$editor->setRequiredFields('te_name', 'te_email', 'te_datetime', 'te_age', 'te_desc');

$editor->setDefaultOrderby('te_id');
$editor->setDefaultValues(array('te_id'   => '0',
                                
'te_live' => 'NOT LIVE'));

//$editor->addAdditionCallback(create_function('$data', 'foreach($data as $k => $v) {$body[] = "$k => $v";} mail("joe@example.com", "Row added", implode("\n", $body));'));
//$editor->addEditCallback(create_function('$data', 'foreach($data as $k => $v) {$body[] = "$k => $v";} mail("joe@example.com", "Row edited", implode("\n", $body));'));
//$editor->addCopyCallback(create_function('$data', 'foreach($data as $k => $v) {$body[] = "$k => $v";} mail("joe@example.com", "Row copied", implode("\n", $body));'));
//$editor->addDeleteCallback(create_function('$data', 'foreach($data as $k => $v) {$body[] = "$k => $v";} mail("joe@example.com", "Row deleted", implode("\n", $body));'));

function validateAge(&$obj, $data)
{
    
$data = (int)$data;

    if (
$data < 18 OR $data > 80) {
        
$obj->addError('Invalid age! Please enter an age between 18 and 80');
    }

    return
$data;
}

$editor->addValidationCallback('te_age', 'validateAge');

$editor->addDisplayFilter('te_desc', create_function('$v', 'return substr($v, 0, 100) . "...";'));

$editor->display();

?>

How to...

Coming...

Public Method List

The following are the public methods of the object which can be used to manipulate the gui:

  • Constructor(resource database_connection, string table_name)
    Pass two things to the constructor: your MySQL database connection resource, and also the name of the table you wish to be edited.

  • addError(string error_msg)
    Used to add errors to be displayed. Commonly used from within data validation callbacks. This adds a general error which is displayed at the top of the screen.

  • setContextualError(string field_name, string error_msg)
    Used to add errors to be displayed. Commonly used from within data validation callbacks. This adds a contextual error which is displayed next to the appropriate field on the add/edit/copy pages.

  • setConfig(string name, mixed value)
    This allows you to set any of the valid configuration parameters. These currently are:
    Name Details
    perPage Defines how many rows of data to show per page. This is an integer.
    allowPKEditing Determines whether to allow editing of the primary key. This defaults to false. As of 11th April 2005 this is slightly buggy when set to true. This is a boolean.
    useFunctions Whether to use the built in functions on the adding/editing pages. Defaults to false. This is a boolean.
    functions An array of built in functions to facilitate easy entering of things like md5 hashes, current date and/or time etc. This is an associative array where the key is what's displayed on the add/edit page, and the value is a valid PHP callback which returns the desired value. Any value entered by the user on the add/edit page is passed to the callback as an argument.
    allowView Whether to allow viewing of rows. Defaults to allowed. This is a boolean.
    allowAdd Whether to allow adding of rows. Defaults to allowed. This is a boolean.
    allowCopy Whether to allow copying of rows. Defaults to allows. This is a boolean.
    allowEdit Whether to allow editing of rows. Defaults to allowed. This is a boolean.
    allowDelete Whether to allow deletion of rows. Defaults to allowed. This is a boolean.
    allowASearch Whether to allow use of the advanced search. Defaults to allowed. This is a boolean.
    allowCSV Whether to allow CSV downloads. Defaults to allowed. This is a boolean.
    headerfile Don't like my header? Use this to define an external file to use instead. All CSS will have to be defined or it will look... well... ugly. This is a string.
    footerfile As above but for the footer. This is a string.


  • getConfig(string name)
    Retrieve one of the various configuration options. See setConfig().

  • setSearchableFields(string field_name [, ...])
    One or more field names which are allowed to be searched on. When using the setValuesFrom*() methods, the displayed values are searched, not the values in the database. This should make it more intuitive for the user. These searchable fields also apply to the advanced search.

  • setRequiredFields(string field_name [, ...])
    One or more field names which are required to be entered (ie not left blank) on the add/copy/edit pages.

  • setDefaultOrderby(string field [, integer direction])
    Use this to set the initial ordering of the data displayed. Pass in the fieldname to the function, and optionally a direction (1 = ascending, 0 = descending).

  • setDefaultValues(array default_values)
    Use this to set default values when adding new records. You can set default values here for fields that aren't shown to the user on the add page (Eg. You might want to set new records to "not live" until moderated).

  • setDisplayName(array display_names)
    This will probably be the most commonly used function. It allows you to display user friendly column names instead of the actual column names. Pass in an associative array of which the key should be the actual column name, and the value should be the display name.

  • setValuesFromQuery(string field_name, string query)
    If you have foreign keys in your table, and instead of displaying the value in the table, you want to display the value in the "other" table, then you can use this to grab the values from the other table. The query should return two columns, the first should be the value which is in the table being edited, and the second should be the value which you wish to be displayed.

  • setValuesFromArray(string field_name, array values)
    This is similar to the above, except that instead of a query, you supply a predetermined array of the values. The array should be associative with the keys being the values stored in the table being edited, and the values (of the array) being the values you want to be displayed.

  • setInputType(string field_name, string input_type)
    Commonly used with one of the above two methods. You can use this function to set the input type shown on the add/edit pages. In use with the above two functions you would set the input type to 'select', so that only one of the preset values can be entered. An example is listed above in the comments script where the type is set to 'select' and the values are set to the news article titles.

  • noDisplay(string field_name [, ...])
    Use this function to prevent the display of fields. You can supply one or more field names to the function. This will not hide the fields from the add/edit pages, use the noEdit() method for that.

  • noEdit(string field_name [, ...])
    If you wish to display a field, but not allow its editing, then use this function. Like the above, you can supply one or more field names to it.

  • onlyDisplay(string field_name [, ...])
    Use this function to set the fields which will be displayed. Only those specified will be displayed, any others will be set to be hidden.

  • onlyEdit(string field_name [, ...])
    Use this function to set the fields which will be editable. Only those specified will be editable, any others will be set to be hidden.

  • addDataFilter(string clause [, ...])
    Adds a data filter. Works purely during display, not add/edit. You should supply one or more valid SQL WHERE clauses which produce the desired results. Eg. addDataFilter("te_desc != ''", "te_email != ''")

  • addDisplayFilter(string field_name, callback callback)
    This function can be used to manipulate the table data before it's displayed. One example as shown above in the first code listing is to prevent the full display of text fields. The function in the listing str_curtail() is a user function which cuts off text after the specified number of characters (though will not cut off mid-word), and then appends "...". The value of the field is passed as an argument to the callback, and the callback should return the result.

  • addValidationCallback(string field_name, callback callback)
    Adds a data validation callback to the given field name. These are run after functions, defaults, required field checks and pseudo-field validation checks. Using these you can add your own validation checks before data is added/updated in the database. These callbacks may also modify the data (eg. md5()ing a password). Now these are added, functions default to off since they're pretty much redundant.

    The callback must:
    • Accept two arguments: the first is the TableEditor object, and the second is the data to validate. It is important for error handling purposes that the first argument is passed by reference. This is only an issue if you're using PHP4.
    • Return the value which is to be inserted into the database. This is to allow modification of data before insertion. You can of course return the supplied data untouched if you only wish to validate it.
    • Call the addError() method on the TableEditor object if an error occurs to register the error. The error should naturally be descriptive.


  • addAdditionCallback(callback callback)
    This function can be used to specify a callback function that should be run on the successful addition of a new record. The new data that has been added is passed as an argument to the callback. This could be used if you wish to log actions performed by a user. You can call this function multiple times to add more than one callback.

  • addCopyCallback(callback callback)
    As above but for copies.

  • addEditCallback(callback callback)
    As above but for edits.

  • addDeleteCallback(callback callback)
    As above but for deletes.

  • display()
    Call this after you've configured the object to display the page.

Dependencies

  • The PEAR package "Pager" must be installed and available via the include_path, a relatively recent version that supports "Pager_Sliding" must be used.
  • The PEAR package "Net_URL" must be installed and available via the include_path.

ChangeLog

16th June 2005 (1.0.6)
==============

 o Fixed bug when not displaying primary key field.
 o Fixed javascript bug in FireFox when selecting records.
 o Added CSV download option. Can download either the selected records, the current
   page or the entire table. Works well with searching too (ie. you can download the
   current page even if it's a search result). There's a callback available to
   override which escapes a single field of CSV data. The built in escape function
   replaces carriage returns and line feeds with \r and \n respectively. It also
   escapes commas with a backslash.
 o Added advanced search capabilities


26th April 2005 (1.0.5)
===============

 o Added support for join tables. Using these you can join the main table onto other
   tables to retrieve columns from the "other" tables. When using join tables add
   copy and delete functionality is automatically disabled, however edits can still
   take place.
 o Added two new methods, onlyDisplay() and onlyEdit(). As the names suggest these
   methods set all fields to be hidden except those specified. Useful if you join
   onto tables and end up having to hide lots of columns.
 o Backported to PHP4. Be careful with references when using data validation callbacks.


19th April 2005 (1.0.4)
===============

 o Added support for a "bitmask" type. Descriptive values need to be set using the
   setValuesFrom*() methods. On the add/edit pages, this type is shown as a multiple
   select. The corresponding type in MySQL is a SET, however you could just as easily
   use an int.
 o Added a "Set blank password" checkbox to the "password" pseudo input, to allow
   setting of a blank password.
 o Changed field specific errors on add/editcopy page to be next to their respective
   fields. There's now a new method called setContextualError() to allow setting of
   these errors from data validation callbacks.


17th April 2005 (1.0.3)
===============

 o Fixed a bug which made multiple display filters on a column not work correctly.
 o Search now highlights found terms. Does this by applying a display filter to each
   searchable colum, which is applied *after* any user defined display filters.


15th April 2005 (1.0.2)
===============

 o No longer classed as beta, (well, by me at least... ).
 o When using the view button, fields with preset values are now shown
   as they are on the main table page.
 o Fixed a bug which meant edit was disabled when view was, and couldn't be
   disabled by itself.
 o Fixed redirection bug with urls.
 o Fixed leading zeros being removed when inserting purely numeric values into
   a text based field type.
 o Added row copying functionality.
 o Added data filters via addDataFilter() method to facilitate the
   showing/viewing/editing/deleting of only certain rows
 o Table structure is now no longer determined on each and every page refresh, but
   is cached in the session.
 o New method: setSearchableFields(). To be used instead of setConfig('searchableFields').
   Supply one or more field names which are allowed to be searched.
 o Added support for required fields when adding/editing/copying. If a required field is
   not filled in, an error is displayed.
 o Added support for pseudo input types:
    o password  This shows two password inputs on the add/edit page. If when
                submitted the two do not match, an error is displayed. However,
                if the two are left blank/empty, the field is not updated. This
                is to allow updates to rows without changing the password field.
                You can alter this behaviour by stipulating the field to be
                required.
    o email     This shows a regular text input, however it automatically checks
                whatever contents are supplied and ensures it matches the form of
                an email address. If however the field is empty, no error is
                raised. You can alter this behaviour by stipulating the field to
                be required.
 o Added support for data validation callbacks. These are run after functions, defaults,
   required field checks and pseudo field validation checks. Using these you can add your
   own validation checks before data is added/updated in the database. These callbacks
   may also modify the data (eg. md5()ing a password). Now these are added, functions
   default to off since along with default values functions are pretty much redundant.
 o Added handling for magic_quotes_gpc


14th April 2005 (1.0.1-beta)
===============

 o Changed aesthetics for shameful self promotion
 o Added GPL licensing
 o noDisplay() method now only hides a field from display, not editing. Use
   noEdit() in combination to hide a field from editing too.
 o Renamed defaultOrderby() to setDefaultOrderby()
 o Fixed a number of E_NOTICE errors
 o Added setDefaultValues(), which sets default values when adding rows. Can
   be used with fields that aren't editable.
 o Added add/edit/delete callback function support via addAdditionCallback(),
   addEditCallback() and addDeleteCallback() methods. The callbacks are run when
   each appropriate action is taken.
 o Added ability to search fields which have their values preset with the
   setValuesFrom* methods.
 o Changed to using a single checkbox per row, and a single set of add/edit/delete
   buttons. Can now delete multiple rows at once.
 o Added View button, for viewing a row in full on it's own page.
 o Added support for external header and footer files. Changing the header will lose
   all CSS so you'll have to define your own.
 o You can now disable any/all of the view/add/edit/delete buttons.


11th April 2005 (1.0.0-beta)
===============

 o Initial release

TODO List

My current list of features to add:

  • Advanced Search: present a page with separate inputs for each searchable field
  • Multiple column primary keys
  • NULL support
  • Auto_increment recognition
  • Check that, when adding, fields with preset value lists contain one of the preset values.
RSS Feed for Comments

Comments

Author: Kolia
Posted: 14th April 2005 08:33
Big thanks ty :)
You did a very useful module.

Btw, do you know any other such modules?

And my addition to your TODO list: would be great to make an option to show not all table but only rows that match some condition. Something like Search function, but not allowing to see not matching rows.
Author: Kolia
Posted: 14th April 2005 11:38
Found one trouble. I have to store in a DB phone codes which are integers starting with 0. When adding them to DB leading zero lost. In the DB they are stored as varchar.
Author: Kolia
Posted: 14th April 2005 11:42
fixed that by commenting "is_numeric" part in dbQuote.
Author: Richard Heyes
Posted: 14th April 2005 13:09
Kolia:
> fixed that by commenting "is_numeric" part in
> dbQuote.

Great. Thanks!
Author: Richard@Home
Posted: 15th April 2005 17:13
Just a quick question to Kolia:

Why are you storing telphone numebers as integers?

Integers don't have leading zero's.

Wouldn't it be better to store them as strings?

That would allow you to have international telephone numbers too (+44)0114 xxxxxx (for example)

Storing them as strings will also allow you to perform searches such as

SELECT * FROM telephone WHERE number LIKE '(+44)%'
Author: Richard Heyes
Posted: 15th April 2005 17:54
Richard@Home:
> Just a quick question to Kolia:
>
> Why are you storing telphone numebers as
> integers?
>
> Integers don't have leading zero's.
>
> Wouldn't it be better to store them as
> strings?
>
> That would allow you to have international
> telephone numbers too (+44)0114 xxxxxx (for
> example)
>
> Storing them as strings will also allow you to
> perform searches such as
>
> SELECT * FROM telephone WHERE number LIKE
> '(+44)%'
>

He probably wasn't. It was a valid bug in the code that occurred when the field type was textual.
Author: Kolia
Posted: 16th April 2005 19:36
Richard Heyes is right. i store them as varchar.

It works without the is_numeric part in MySQL because MySQL understands numeric values in quotes. But I don't know will it work in other DBMS.

And the question about other similar modules left unanswered...
Author: Richard Heyes
Posted: 16th April 2005 19:40
Kolia:
> And the question about other similar modules
> left unanswered...

See the original article entitled "Table Editing". Or Google.
Author: Arif Ender
Posted: 27th April 2005 07:41
Great stuff, Richard. Thank you!
Author: ravi
Posted: 13th June 2005 11:43
This is a great module but do you think this will work for Postgresql as well?
Since you use pear that seems to be a good foundation.

I have views on Postgres that I want to make editable and searchable.

All the best.
Author: Richard Heyes
Posted: 13th June 2005 12:07
ravi:
> This is a great module but do you think this
> will work for Postgresql as well?
> Since you use pear that seems to be a good
> foundation.
>
> I have views on Postgres that I want to make
> editable and searchable.

Well, I guess it's possible. You'd have to separate out all the code which determines the table structure and field types, but after you've done that there shouldn't be all that much else to change.
Author: Tony
Posted: 14th June 2005 17:52
Hello.

I really admire the fact you submit classes to the php community that are so handy! The time a class like this saves when I'm working on the backend of PHP applications is amazing.

I was looking around and actually found a very similar thing at: http://platon.sk/projects/main_page.php?project_id=5 . I haven't tried it yet, but it looks like more of a "program that does it all for you" as opposed to what your giving us. A class saves programmer's time, and at the same times requires programming knowledge and effort to get working. Just alot less time. :o)

Ahh -- I hope you update this thing more! It's absolutely beautiful! Please don't abandon this thing! :o)

- Tony
P.S. Nifty bot-protection ^_^
Author: Richard Heyes
Posted: 14th June 2005 17:57
Tony:

> Ahh -- I hope you update this thing more! It's
> absolutely beautiful! Please don't abandon this
> thing! :o)

Glad you like it. I'm still developing it, CSV download is already added (selected rows, current page or entire table), and I'm half way through adding an advanced search facility.
Author: AlexIsWorkingForAChange
Posted: 29th June 2005 15:33
regarding using function setConfig($name, $value) to set a header, footer or both:

If you've set up your include paths, and wish to include a header which is not in the same directory as the script which is including the tableeditor, its quite possible that file_exists($fileName) will return false even though the file is included quite happily.

In this case, to avoid the spurious "Failed to find headerfile..." error, just comment out these 2 lines (253 - 254 on my copy):

===========
$this->errors[] = "Failed to find $name: " . htmlspecialchars($value);
return;
===========

HTH someone..
Author: marcus
Posted: 8th July 2005 08:51
hi, i like that, but don't get it to run :-(
Error: First argument is not a valid database connection!

where is the $conn Var defined?
i cant't find anything to write in my DB connection data...

please help - i'm not so good in PHP

greetings marcus
Author: Martin
Posted: 2nd August 2005 17:30
Marcus, connect like this:

require_once('TableEditor.php');

$database = mysql_connect('localhost', 'user', 'passwd')
or die('Could not connect: ' . mysql_error());
echo 'Connected successfully';
mysql_select_db('name-of-db') or die('Could not select database');

$editor = new TableEditor($database, 'table-name');


I just started using this great class and am considering porting it to use pear::db instead of raw mysql. Do you think it would fly or is it pear::db not specific or rubust enough to support TableEditor?
Author: Richard Heyes
Posted: 2nd August 2005 17:34
Martin:
> I just started using this great class and am
> considering porting it to use pear::db instead
> of raw mysql. Do you think it would fly or is
> it pear::db not specific or rubust enough to
> support TableEditor?

The problem you'll have is having to write code that gets the structure of the table that works cross database. I guess it's feasible.
Author: Martin
Posted: 4th August 2005 17:18
Ok,

I figured out how to do joins with addJoinTable()

I am using a legacy database that seems to have the name of the key of every table named ID. To deal with this I would use: table1.id, table2.id... in my sql.

Is there an easy way to get TableEditor to refer to fields by tablename.fieldname, or tell it to refer to specific fields with that nomenclature?

Thanks for the help.
Martin
Author: Martin
Posted: 4th August 2005 22:07
I tried a simple hack:

changing the $this>addField($row['Field'], ... to

$this>addField($table.'.'.$row['Field'], ...

in all the case statments of getStructure()

This had some success, I am now able to join tables with duplicate field names, but now the table display seems to loose the first field of first joied field.

For example if the first joined table was: table2.id, table2.name, table2.address; On the display page under table2.name would be the fields for table2.address

Author: Arsen
Posted: 26th August 2005 19:27
Hi,

I can't set default values for hidden fields. I get 'ERROR: Failed to find specified row in database' after pressing Apply.

I hide the field in Display/Edit and set default value for it:

noEdit('myFieldName');
setDefaultValues(array('myFieldName' => 'MyValue');

And DB still gets updated where 'myFieldName' value = NULL.

Thanks
Author: Arsen
Posted: 26th August 2005 19:32
Forgot to mention that I also set

AddDataFilter('myFieldName=MyValue');

I get the error because of the data filter.
Seems like setDefaultValues method does not work on Hidden fields.
Author: sam adi
Posted: 26th September 2005 01:05
how i can add two field and display ini one table,
and I Can't dropdowns choise to add data table.

Please help me, I am newbee

thank's before

Author: sam adi
Posted: 26th September 2005 01:18
sory my question wrong

how I Can sum data in two fields and display in another field

and I Can't dropdowns choise to add data table.

thank's Before

Post Comment

Your name:
Your email: (Don't worry, I won't spam your ass.)
Comments:
  Do not post support type questions please

  To prevent spamming, enter the following number numerically in the following text input. Eg. For "two hundred and two", you would enter "202" (no quotes).
  five thousand four hundred and sixty-four
Number: