diff --git a/include/ajax.forms.php b/include/ajax.forms.php index 1ec760b077e1c934ee13cffa568147eb22897344..b6e5d2903b3ea7a7f41afbb3c666a3656a550e05 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -50,5 +50,22 @@ class DynamicFormsAjaxAPI extends AjaxController { $ent->delete(); } + + function getListItemProperties($item_id) { + if (!($item = DynamicListItem::lookup($item_id))) + Http::response(404, 'No such list item'); + + include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'); + } + + function saveListItemProperties($item_id) { + if (!($item = DynamicListItem::lookup($item_id))) + Http::response(404, 'No such list item'); + + if (!$item->setConfiguration()) + include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php'); + else + $item->save(); + } } ?> diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index c1b790ccb46410923fcd11364539bd0ed057ad4a..d9914e51d230e76fe8dbd80b3512871964065c0c 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -789,6 +789,7 @@ class DynamicList extends VerySimpleModel { ); var $_items; + var $_form; function getSortModes() { return array( @@ -831,6 +832,14 @@ class DynamicList extends VerySimpleModel { ->count(); } + function getConfigurationForm() { + if (!$this->_form) { + $this->_form = DynamicForm::lookup( + array('type'=>'L'.$this->get('id'))); + } + return $this->_form; + } + function save($refetch=false) { if (count($this->dirty)) $this->set('updated', new SqlFunction('NOW')); @@ -892,6 +901,49 @@ class DynamicListItem extends VerySimpleModel { ), ); + var $_config; + var $_form; + + function getConfiguration() { + if (!$this->_config) { + $this->_config = $this->get('configuration'); + if (is_string($this->_config)) + $this->_config = JsonDataParser::parse($this->_config); + elseif (!$this->_config) + $this->_config = array(); + } + return $this->_config; + } + + function setConfiguration(&$errors=array()) { + $config = array(); + foreach ($this->getConfigurationForm()->getFields() as $field) { + $config[$field->get('id')] = $field->to_database($field->getClean()); + $errors = array_merge($errors, $field->errors()); + } + if (count($errors) === 0) + $this->set('configuration', JsonDataEncoder::encode($config)); + + return count($errors) === 0; + } + + function getConfigurationForm() { + if (!$this->_form) { + $this->_form = DynamicForm::lookup( + array('type'=>'L'.$this->get('list_id'))); + } + return $this->_form; + } + + + function getVar($name) { + $config = $this->getConfiguration(); + foreach ($this->getConfigurationForm()->getFields() as $field) { + if (strcasecmp($field->get('name'), $name) === 0) + return $config[$field->get('id')]; + } + } + function toString() { return $this->get('value'); } diff --git a/include/class.nav.php b/include/class.nav.php index 992daf107f8f8c212d4fc5528a9912e0f5e21e1b..c744e24d141e4ec319ed1eef1bd45345bc33869d 100644 --- a/include/class.nav.php +++ b/include/class.nav.php @@ -100,6 +100,8 @@ class StaffNav { $this->tabs['users'] = array('desc' => 'Users', 'href' => 'users.php', 'title' => 'User Directory'); $this->tabs['tickets'] = array('desc'=>'Tickets','href'=>'tickets.php','title'=>'Ticket Queue'); $this->tabs['kbase'] = array('desc'=>'Knowledgebase','href'=>'kb.php','title'=>'Knowledgebase'); + // TODO: If at least one app is installed + $this->tabs['apps']=array('desc'=>'Applications','href'=>'apps.php','title'=>'Applications'); } return $this->tabs; @@ -148,6 +150,9 @@ class StaffNav { $subnav[]=array('desc'=>'Canned Responses','href'=>'canned.php','iconclass'=>'canned'); } break; + case 'apps': + $subnav[]=array('desc'=>'Equipment', 'href'=>'apps?a=equipment','iconclass'=>'icon-bug'); + break; } if($subnav) $submenus[$this->getPanel().'.'.strtolower($k)]=$subnav; diff --git a/include/class.orm.php b/include/class.orm.php index ef04e3d3e9bb7bb2412c074e9c73e2c57d3d9157..a5035bc3edd1575a5076c918f70b235a8fb21048 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -309,6 +309,11 @@ class QuerySet implements IteratorAggregate, ArrayAccess { return $this->getIterator()->asArray(); } + function one() { + $this->limit(1); + return $this[0]; + } + function count() { $class = $this->compiler; $compiler = new $class(); diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php index 4beb705768cf298e1d62255a4bdc16334066642b..d05396f99732f9ed03064f867eca1778cbfdafa7 100644 --- a/include/staff/dynamic-list.inc.php +++ b/include/staff/dynamic-list.inc.php @@ -22,6 +22,17 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="a" value="<?php echo $_REQUEST['a']; ?>"> <input type="hidden" name="id" value="<?php echo $info['id']; ?>"> <h2>Custom List</h2> + +<ul class="tabs"> + <li><a href="#definition" class="active"> + <i class="icon-plus"></i> Definition</a></li> + <li><a href="#items"> + <i class="icon-list"></i> Items</a></li> + <li><a href="#properties"> + <i class="icon-asterisk"></i> Properties</a></li> +</ul> + +<div id="definition" class="tab_content"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <tr> @@ -52,7 +63,128 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </select></td> </tr> </tbody> + <tbody> + <tr> + <th colspan="7"> + <em><strong>Internal Notes:</strong> be liberal, they're internal</em> + </th> + </tr> + <tr> + <td colspan="7"><textarea name="notes" class="richtext no-bar" + rows="6" cols="80"><?php + echo $info['notes']; ?></textarea> + </td> + </tr> + </tbody> </table> +</div> +<div id="properties" class="tab_content" style="display:none"> + <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> + <thead> + <tr> + <th colspan="7"> + <em><strong>Item Properties</strong> properties definable for each item</em> + </th> + </tr> + <tr> + <th nowrap>Sort + <i class="help-tip icon-question-sign" href="#field_sort"></i></th> + <th nowrap>Label + <i class="help-tip icon-question-sign" href="#field_label"></i></th> + <th nowrap>Type + <i class="help-tip icon-question-sign" href="#field_type"></i></th> + <th nowrap>Variable + <i class="help-tip icon-question-sign" href="#field_variable"></i></th> + <th nowrap>Delete + <i class="help-tip icon-question-sign" href="#field_delete"></i></th> + </tr> + </thead> + <tbody class="sortable-rows" data-sort="sort-"> + <?php if ($form) foreach ($form->getDynamicFields() as $f) { + $id = $f->get('id'); + $deletable = !$f->isDeletable() ? 'disabled="disabled"' : ''; + $force_name = $f->isNameForced() ? 'disabled="disabled"' : ''; + $fi = $f->getImpl(); + $ferrors = $f->errors(); ?> + <tr> + <td><i class="icon-sort"></i></td> + <td><input type="text" size="32" name="label-<?php echo $id; ?>" + value="<?php echo Format::htmlchars($f->get('label')); ?>"/> + <font class="error"><?php + if ($ferrors['label']) echo '<br/>'; echo $ferrors['label']; ?> + </td> + <td nowrap><select name="type-<?php echo $id; ?>" <?php + if (!$fi->isChangeable()) echo 'disabled="disabled"'; ?>> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { + if ($f->get('type') != $type + && isset($nfo[2]) && !$nfo[2]) continue; ?> + <option value="<?php echo $type; ?>" <?php + if ($f->get('type') == $type) echo 'selected="selected"'; ?>> + <?php echo $nfo[0]; ?></option> + <?php } ?> + </optgroup> + <?php } ?> + </select> + <?php if ($f->isConfigurable()) { ?> + <a class="action-button" style="float:none;overflow:inherit" + href="ajax.php/form/field-config/<?php + echo $f->get('id'); ?>" + onclick="javascript: + $('#overlay').show(); + $('#field-config .body').load(this.href); + $('#field-config').show(); + return false; + "><i class="icon-edit"></i> Config</a> + <?php } ?></td> + <td> + <input type="text" size="20" name="name-<?php echo $id; ?>" + value="<?php echo Format::htmlchars($f->get('name')); + ?>" <?php echo $force_name ?>/> + <font class="error"><?php + if ($ferrors['name']) echo '<br/>'; echo $ferrors['name']; + ?></font> + </td> + <td><input type="checkbox" name="delete-<?php echo $id; ?>" + <?php echo $deletable; ?>/> + <input type="hidden" name="sort-<?php echo $id; ?>" + value="<?php echo $f->get('sort'); ?>"/> + </td> + </tr> + <?php + } + for ($i=0; $i<$newcount; $i++) { ?> + <td><em>+</em> + <input type="hidden" name="sort-new-<?php echo $i; ?>" + value="<?php echo $info["sort-new-$i"]; ?>"/></td> + <td><input type="text" size="32" name="label-new-<?php echo $i; ?>" + value="<?php echo $info["label-new-$i"]; ?>"/></td> + <td><select name="type-new-<?php echo $i; ?>"> + <?php foreach (FormField::allTypes() as $group=>$types) { + ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php + foreach ($types as $type=>$nfo) { + if (isset($nfo[2]) && !$nfo[2]) continue; ?> + <option value="<?php echo $type; ?>" + <?php if ($info["type-new-$i"] == $type) echo 'selected="selected"'; ?>> + <?php echo $nfo[0]; ?> + </option> + <?php } ?> + </optgroup> + <?php } ?> + </select></td> + <td><input type="text" size="20" name="name-new-<?php echo $i; ?>" + value="<?php echo $info["name-new-$i"]; ?>"/> + <font class="error"><?php + if ($errors["new-$i"]['name']) echo '<br/>'; echo $errors["new-$i"]['name']; + ?></font> + <td></td> + </tr> + <?php } ?> + </tbody> +</table> +</div> +<div id="items" class="tab_content" style="display:none"> <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2"> <thead> <?php if ($list) { @@ -77,6 +209,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <th>Delete</th> </tr> </thead> + <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?> class="sortable-rows" data-sort="sort-"<?php } ?>> <?php if ($list) @@ -90,7 +223,18 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="sort-<?php echo $id; ?>" value="<?php echo $i->get('sort'); ?>"/></td> <td><input type="text" size="40" name="value-<?php echo $id; ?>" - value="<?php echo $i->get('value'); ?>"/></td> + value="<?php echo $i->get('value'); ?>"/> + <?php if ($form->getFields()) { ?> + <a class="action-button" style="float:none;overflow:inherit" + href="ajax.php/list/item/<?php + echo $i->get('id'); ?>/properties" + onclick="javascript: + $('#overlay').show(); + $('#field-config .body').load(this.href); + $('#field-config').show(); + return false; + "><i class="icon-edit"></i> Properties</a> + <?php } ?></td> <td><input type="text" size="30" name="extra-<?php echo $id; ?>" value="<?php echo $i->get('extra'); ?>"/></td> <td> @@ -108,24 +252,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </tr> <?php } ?> </tbody> - <tbody> - <tr> - <th colspan="7"> - <em><strong>Internal Notes:</strong> be liberal, they're internal</em> - </th> - </tr> - <tr> - <td colspan="7"><textarea name="notes" class="richtext no-bar" - rows="6" cols="80"><?php - echo $info['notes']; ?></textarea> - </td> - </tr> - </tbody> - </table> </table> +</div> <p class="centered"> <input type="submit" name="submit" value="<?php echo $submit_text; ?>"> <input type="reset" name="reset" value="Reset"> <input type="button" name="cancel" value="Cancel" onclick='window.location.href="?"'> </p> </form> + +<div style="display:none;" class="dialog draggable" id="field-config"> + <div class="body"></div> +</div> diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..0a2ba9635df68b0b1a2b61795b1a4bb89e3e6e8e --- /dev/null +++ b/include/staff/templates/list-item-properties.tmpl.php @@ -0,0 +1,68 @@ + <h3>Item Properties — <?php echo $item->get('value') ?></h3> + <a class="close" href=""><i class="icon-remove-circle"></i></a> + <hr/> + <form method="post" action="ajax.php/list/item/<?php + echo $item->get('id'); ?>/properties" onsubmit="javascript: + var form = $(this); + $.post(this.action, form.serialize(), function(data, status, xhr) { + if (!data.length) { + form.closest('.dialog').hide(); + $('#overlay').hide(); + } else { + form.closest('.dialog').empty().append(data); + } + }); + return false;"> + <table width="100%"> + <?php + echo csrf_token(); + $config = $item->getConfiguration(); + foreach ($item->getConfigurationForm()->getFields() as $f) { + $name = $f->get('id'); + if (isset($config[$name])) + $f->value = $config[$name]; + else if ($f->get('default')) + $f->value = $f->get('default'); + ?> + <tr><td class="multi-line"> + <label for="<?php echo $f->getWidget()->name; ?>" + style="vertical-align:top;padding-top:0.2em"> + <?php echo Format::htmlchars($f->get('label')); ?>:</label> + </td><td> + <span style="display:inline-block"> + <?php + $f->render(); + if ($f->get('required')) { ?> + <font class="error">*</font> + <?php + } + if ($f->get('hint')) { ?> + <br /><em style="color:gray;display:inline-block"><?php + echo Format::htmlchars($f->get('hint')); ?></em> + <?php + } + ?> + </span> + <?php + foreach ($f->errors() as $e) { ?> + <br /> + <font class="error"><?php echo $e; ?></font> + <?php } ?> + </td></tr> + <?php + } + ?> + </table> + <hr> + <p class="full-width"> + <span class="buttons" style="float:left"> + <input type="reset" value="Reset"> + <input type="button" value="Cancel" class="close"> + </span> + <span class="buttons" style="float:right"> + <input type="submit" value="Save"> + </span> + </p> + </form> + <div class="clear"></div> + diff --git a/scp/ajax.php b/scp/ajax.php index 5d12a8b43ebc215fce392f197b203bf842cfd1ec..65ce1c01143df2976f82c3ad2b21f97285af15f4 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -56,6 +56,10 @@ $dispatcher = patterns('', url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'), url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer') )), + url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI', + url_get('^item/(?P<id>\d+)/properties$', 'getListItemProperties'), + url_post('^item/(?P<id>\d+)/properties$', 'saveListItemProperties') + )), url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI', # Send url_get('^graph$', 'getPlotData'), diff --git a/scp/lists.php b/scp/lists.php index a68267703661f332df7f8b5f4ad4d973b59f60cd..06e52d8d5611d920f06f5ffa827a3be5396d357a 100644 --- a/scp/lists.php +++ b/scp/lists.php @@ -6,6 +6,10 @@ $list=null; if($_REQUEST['id'] && !($list=DynamicList::lookup($_REQUEST['id']))) $errors['err']='Unknown or invalid dynamic list ID.'; +if ($list) { + $form = DynamicForm::lookup(array('type'=>'L'.$_REQUEST['id'])); +} + if($_POST) { $fields = array('name', 'name_plural', 'sort_mode', 'notes'); $required = array('name'); @@ -35,6 +39,39 @@ if($_POST) { $item->set($i, $_POST["$i-$id"]); $item->save(); } + + $names = array(); + foreach ($form->getDynamicFields() as $field) { + $id = $field->get('id'); + if ($_POST["delete-$id"] == 'on' && $field->isDeletable()) { + $field->delete(); + // Don't bother updating the field + continue; + } + if (isset($_POST["type-$id"]) && $field->isChangeable()) + $field->set('type', $_POST["type-$id"]); + if (isset($_POST["name-$id"]) && !$field->isNameForced()) + $field->set('name', $_POST["name-$id"]); + # TODO: make sure all help topics still have all required fields + foreach (array('sort','label') as $f) { + if (isset($_POST["$f-$id"])) { + $field->set($f, $_POST["$f-$id"]); + } + } + if (in_array($field->get('name'), $names)) + $field->addError('Field variable name is not unique', 'name'); + if (preg_match('/[.{}\'"`; ]/u', $field->get('name'))) + $field->addError('Invalid character in variable name. Please use letters and numbers only.', 'name'); + if ($field->get('name')) + $names[] = $field->get('name'); + if ($field->isValid()) + $field->save(); + else + # notrans (not shown) + $errors["field-$id"] = 'Field has validation errors'; + // Keep track of the last sort number + $max_sort = max($max_sort, $field->get('sort')); + } break; case 'add': foreach ($fields as $f) @@ -53,7 +90,6 @@ if($_POST) { $msg = 'Custom list added successfully'; else $errors['err'] = 'Unable to create custom list. Unknown internal error'; - break; case 'mass_process': @@ -96,6 +132,28 @@ if($_POST) { # Invalidate items cache $list->_items = false; } + + if ($form) { + for ($i=0; isset($_POST["sort-new-$i"]); $i++) { + if (!$_POST["label-new-$i"]) + continue; + $field = DynamicFormField::create(array( + 'form_id'=>$form->get('id'), + 'sort'=>$_POST["sort-new-$i"] ? $_POST["sort-new-$i"] : ++$max_sort, + 'label'=>$_POST["label-new-$i"], + 'type'=>$_POST["type-new-$i"], + 'name'=>$_POST["name-new-$i"], + )); + $field->setForm($form); + if ($field->isValid()) + $field->save(); + else + $errors["new-$i"] = $field->errors(); + } + // XXX: Move to an instrumented list that can handle this better + if (!$errors) + $form->_dfields = $form->_fields = null; + } } $page='dynamic-lists.inc.php';