+
diff --git a/js/forum/src/components/Post.js b/js/forum/src/components/Post.js
index d6b72089a..d86151089 100644
--- a/js/forum/src/components/Post.js
+++ b/js/forum/src/components/Post.js
@@ -49,7 +49,8 @@ export default class Post extends Component {
children: controls,
className: 'Post-controls',
buttonClassName: 'Button Button--icon Button--flat',
- menuClassName: 'Dropdown-menu--right'
+ menuClassName: 'Dropdown-menu--right',
+ icon: 'ellipsis-v'
}) : ''}
{this.content()}
diff --git a/js/forum/src/components/SessionDropdown.js b/js/forum/src/components/SessionDropdown.js
index e0f5ceeb1..a7f8b8b58 100644
--- a/js/forum/src/components/SessionDropdown.js
+++ b/js/forum/src/components/SessionDropdown.js
@@ -62,7 +62,7 @@ export default class SessionDropdown extends Dropdown {
50
);
- if (user.groups().some(group => Number(group.id()) === Group.ADMINISTRATOR_ID)) {
+ if (user.groups().some(group => group.id() === Group.ADMINISTRATOR_ID)) {
items.add('administration',
LinkButton.component({
icon: 'wrench',
diff --git a/js/lib/Model.js b/js/lib/Model.js
index df16a5e63..327e569b5 100644
--- a/js/lib/Model.js
+++ b/js/lib/Model.js
@@ -189,9 +189,10 @@ export default class Model {
method: 'DELETE',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data
- }).then(
- () => this.exists = false
- );
+ }).then(() => {
+ this.exists = false;
+ this.store.remove(this);
+ });
}
/**
@@ -214,7 +215,7 @@ export default class Model {
*/
static attribute(name, transform) {
return function() {
- const value = this.data.attributes[name];
+ const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
};
diff --git a/js/lib/Store.js b/js/lib/Store.js
index 18fbb6c0f..ddef9d480 100644
--- a/js/lib/Store.js
+++ b/js/lib/Store.js
@@ -139,6 +139,15 @@ export default class Store {
return records ? Object.keys(records).map(id => records[id]) : [];
}
+ /**
+ * Remove the given model from the store.
+ *
+ * @param {Model} model
+ */
+ remove(model) {
+ delete this.data[model.data.type][model.id()];
+ }
+
/**
* Create a new record of the given type.
*
diff --git a/js/lib/components/Badge.js b/js/lib/components/Badge.js
index 6e071a6bf..142a1ef0a 100644
--- a/js/lib/components/Badge.js
+++ b/js/lib/components/Badge.js
@@ -20,17 +20,17 @@ export default class Badge extends Component {
const type = extract(attrs, 'type');
const iconName = extract(attrs, 'icon');
- attrs.className = 'Badge Badge--' + type + ' ' + (attrs.className || '');
- attrs.title = extract(attrs, 'label');
+ attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
+ attrs.title = extract(attrs, 'label') || '';
// Give the badge a unique key so that when badges are displayed together,
// and then one is added/removed, Mithril will correctly redraw the series
// of badges.
- attrs.key = attrs.className;
+ attrs.key = attrs.type;
return (
- {iconName ? icon(iconName, {className: 'Badge-icon'}) : ''}
+ {iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust(' ')}
);
}
diff --git a/js/lib/components/Button.js b/js/lib/components/Button.js
index 5991cfe27..a2ae2d7de 100644
--- a/js/lib/components/Button.js
+++ b/js/lib/components/Button.js
@@ -49,7 +49,7 @@ export default class Button extends Component {
const iconName = this.props.icon;
return [
- iconName ? icon(iconName, {className: 'Button-icon'}) : '', ' ',
+ iconName && iconName !== true ? icon(iconName, {className: 'Button-icon'}) : '',
this.props.children ? {this.props.children} : '',
this.props.loading ? LoadingIndicator.component({size: 'tiny', className: 'LoadingIndicator--inline'}) : ''
];
diff --git a/js/lib/components/Dropdown.js b/js/lib/components/Dropdown.js
index e93eb4ca4..c17d96745 100644
--- a/js/lib/components/Dropdown.js
+++ b/js/lib/components/Dropdown.js
@@ -10,9 +10,10 @@ import listItems from 'flarum/helpers/listItems';
*
* - `buttonClassName` A class name to apply to the dropdown toggle button.
* - `menuClassName` A class name to apply to the dropdown menu.
- * - `icon` The name of an icon to show in the dropdown toggle button. Defaults
- * to 'ellipsis-v'.
+ * - `icon` The name of an icon to show in the dropdown toggle button.
+ * - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
+ * - `onhide`
*
* The children will be displayed as a list inside of the dropdown menu.
*/
@@ -23,8 +24,8 @@ export default class Dropdown extends Component {
props.className = props.className || '';
props.buttonClassName = props.buttonClassName || '';
props.contentClassName = props.contentClassName || '';
- props.icon = props.icon || 'ellipsis-v';
props.label = props.label || app.trans('core.controls');
+ props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'caret-down';
}
view() {
@@ -40,6 +41,29 @@ export default class Dropdown extends Component {
);
}
+ config(isInitialized) {
+ if (isInitialized) return;
+
+ // When opening the dropdown menu, work out if the menu goes beyond the
+ // bottom of the viewport. If it does, we will apply class to make it show
+ // above the toggle button instead of below it.
+ this.$().on('shown.bs.dropdown', () => {
+ const $menu = this.$('.Dropdown-menu').removeClass('Dropdown-menu--top');
+
+ $menu.toggleClass(
+ 'Dropdown-menu--top',
+ $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
+ );
+ });
+
+ this.$().on('hide.bs.dropdown', () => {
+ if (this.props.onhide) {
+ this.props.onhide();
+ m.redraw();
+ }
+ });
+ }
+
/**
* Get the template for the button.
*
@@ -65,9 +89,9 @@ export default class Dropdown extends Component {
*/
getButtonContent() {
return [
- icon(this.props.icon, {className: 'Button-icon'}),
+ this.props.icon ? icon(this.props.icon, {className: 'Button-icon'}) : '',
{this.props.label}, ' ',
- icon('caret-down', {className: 'Button-caret'})
+ this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
];
}
}
diff --git a/js/lib/components/GroupBadge.js b/js/lib/components/GroupBadge.js
new file mode 100644
index 000000000..fc2ca1cc6
--- /dev/null
+++ b/js/lib/components/GroupBadge.js
@@ -0,0 +1,16 @@
+import Badge from 'flarum/components/Badge';
+
+export default class GroupBadge extends Badge {
+ static initProps(props) {
+ super.initProps(props);
+
+ if (props.group) {
+ props.icon = props.group.icon();
+ props.style = {backgroundColor: props.group.color()};
+ props.label = typeof props.label === 'undefined' ? props.group.nameSingular() : props.label;
+ props.type = 'group--' + props.group.nameSingular();
+
+ delete props.group;
+ }
+ }
+}
diff --git a/js/lib/components/SelectDropdown.js b/js/lib/components/SelectDropdown.js
index 4230db414..ec785c70b 100644
--- a/js/lib/components/SelectDropdown.js
+++ b/js/lib/components/SelectDropdown.js
@@ -5,23 +5,29 @@ import icon from 'flarum/helpers/icon';
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
* button's label is set as the label of the first child which has a truthy
* `active` prop.
+ *
+ * ### Props
+ *
+ * - `caretIcon`
+ * - `defaultLabel`
*/
export default class SelectDropdown extends Dropdown {
static initProps(props) {
super.initProps(props);
props.className += ' Dropdown--select';
+ props.caretIcon = props.caretIcon || 'sort';
}
getButtonContent() {
const activeChild = this.props.children.filter(child => child.props.active)[0];
- let label = activeChild && activeChild.props.children;
+ let label = activeChild && activeChild.props.children || this.props.defaultLabel;
if (label instanceof Array) label = label[0];
return [
{label}, ' ',
- icon('sort', {className: 'Button-caret'})
+ icon(this.props.caretIcon, {className: 'Button-caret'})
];
}
}
diff --git a/js/lib/models/Group.js b/js/lib/models/Group.js
index 1fb1c616f..31be54173 100644
--- a/js/lib/models/Group.js
+++ b/js/lib/models/Group.js
@@ -8,8 +8,8 @@ class Group extends mixin(Model, {
icon: Model.attribute('icon')
}) {}
-Group.ADMINISTRATOR_ID = 1;
-Group.GUEST_ID = 2;
-Group.MEMBER_ID = 3;
+Group.ADMINISTRATOR_ID = '1';
+Group.GUEST_ID = '2';
+Group.MEMBER_ID = '3';
export default Group;
diff --git a/less/admin/AdminNav.less b/less/admin/AdminNav.less
index 21736abe5..b9d3e1714 100644
--- a/less/admin/AdminNav.less
+++ b/less/admin/AdminNav.less
@@ -70,9 +70,11 @@
}
.container {
width: 100%;
- padding: 0 30px;
margin: 0;
+ .App-content & {
+ padding: 0 30px;
+ }
.App-content > & {
padding: 0;
}
diff --git a/less/admin/EditGroupModal.less b/less/admin/EditGroupModal.less
new file mode 100644
index 000000000..e68293d5e
--- /dev/null
+++ b/less/admin/EditGroupModal.less
@@ -0,0 +1,23 @@
+.EditGroupModal {
+ .Form-group:not(:last-child) {
+ margin-bottom: 30px;
+ }
+ .Badge {
+ margin-right: 5px;
+ vertical-align: 2px;
+ }
+}
+.EditGroupModal-name-input {
+ :first-child {
+ margin-bottom: 1px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ :last-child {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+}
+.EditGroupModal-delete {
+ float: right;
+}
diff --git a/less/admin/PermissionsPage.less b/less/admin/PermissionsPage.less
index 70acd0125..92f74ab33 100644
--- a/less/admin/PermissionsPage.less
+++ b/less/admin/PermissionsPage.less
@@ -8,28 +8,39 @@
text-align: center;
color: @text-color;
font-weight: bold;
+ padding-left: 10px;
+ padding-right: 10px;
}
.Group-name {
display: block;
margin-top: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.Group-icon {
font-size: 14px;
+ margin-top: 2px;
}
.Group--add {
- border: 1px dashed @muted-color;
color: @muted-color;
width: auto;
margin-left: 10px;
font-weight: normal;
+
+ .Group-icon {
+ margin-top: 8px;
+ }
}
.PermissionsPage-permissions {
- padding: 30px 0;
+ padding: 30px 0 200px;
+ overflow: auto;
}
.PermissionGrid {
+ white-space: nowrap;
+
td, th {
padding: 10px 0;
text-align: left;
@@ -42,7 +53,11 @@
font-weight: bold;
font-size: 12px;
color: @muted-color;
- width: 140px;
+ min-width: 140px;
+
+ &:not(:hover) .PermissionGrid-removeScope {
+ display: none;
+ }
}
tbody {
th {
@@ -62,12 +77,28 @@
}
.Button {
text-decoration: none;
- }
- td:not(:hover) {
- .Select-caret, .GroupsButton-caret {
- display: none;
+
+ .Badge {
+ margin: -3px 0;
+ vertical-align: 0;
}
}
+ td:not(:hover) .Select-caret,
+ td:not(:hover) .Dropdown:not(.open) .Button-caret {
+ display: none;
+ }
+ .open .Dropdown-toggle {
+ .box-shadow(none);
+ }
+ }
+}
+.PermissionGrid-removeScope {
+ margin: -1px 0;
+}
+.PermissionDropdown {
+ .Badge {
+ margin: -3px 3px -3px 0;
+ vertical-align: 1px;
}
}
.PermissionGrid-section {
diff --git a/less/admin/app.less b/less/admin/app.less
index ba08c19e1..058b10f4a 100644
--- a/less/admin/app.less
+++ b/less/admin/app.less
@@ -4,3 +4,4 @@
@import "DashboardPage.less";
@import "BasicsPage.less";
@import "PermissionsPage.less";
+@import "EditGroupModal.less";
diff --git a/less/lib/Alert.less b/less/lib/Alert.less
index c03b90745..73c731baa 100755
--- a/less/lib/Alert.less
+++ b/less/lib/Alert.less
@@ -15,6 +15,13 @@
color: @alert-error-color;
}
}
+.Alert--success {
+ background: @alert-success-bg;
+
+ &, a, a:hover, button, button:hover {
+ color: @alert-success-color;
+ }
+}
.Alert-controls {
list-style-type: none;
padding: 0;
@@ -39,6 +46,7 @@
> .Button {
margin: -10px;
+ vertical-align: 0;
}
}
}
diff --git a/less/lib/Badge.less b/less/lib/Badge.less
index bb67a29c6..37f5c7fc7 100755
--- a/less/lib/Badge.less
+++ b/less/lib/Badge.less
@@ -1,5 +1,5 @@
.Badge {
- .Badge--size(23px);
+ .Badge--size(24px);
border: 1px solid @body-bg;
background: @muted-color;
color: #fff;
@@ -20,7 +20,7 @@
line-height: @size - 3px;
&, .Badge-icon {
- font-size: 0.56 * @size;
+ font-size: 0.58 * @size;
}
}
diff --git a/less/lib/Button.less b/less/lib/Button.less
index b16ab81d4..89c998cdd 100755
--- a/less/lib/Button.less
+++ b/less/lib/Button.less
@@ -157,12 +157,14 @@
background: transparent !important;
padding: 0;
color: inherit !important;
+ line-height: inherit;
&:hover {
text-decoration: underline;
}
&:active,
- &.active {
+ &.active,
+ .open > &.Dropdown-toggle {
.box-shadow(none);
}
}
@@ -204,6 +206,7 @@
.Button-icon {
font-size: 16px;
vertical-align: -1px;
+ margin: 0;
}
}
.SessionDropdown .Dropdown-toggle {
@@ -214,6 +217,9 @@
.Avatar--size(24px);
}
}
+.Button-icon {
+ margin-right: 3px;
+}
.Button-icon,
.Button-caret {
font-size: 14px;
diff --git a/less/lib/Dropdown.less b/less/lib/Dropdown.less
index e5f10a1e7..2e102d2c7 100755
--- a/less/lib/Dropdown.less
+++ b/less/lib/Dropdown.less
@@ -9,7 +9,7 @@
display: none;
min-width: 160px;
padding: 8px 0;
- margin: 7px 0 0;
+ margin: 7px 0;
background: @body-bg;
border-radius: @border-radius;
.box-shadow(0 2px 6px @shadow-color);
@@ -42,8 +42,10 @@
&.hasIcon {
padding-left: 40px;
}
- &:hover, &:focus {
+ &:hover {
background: @control-bg;
+ }
+ &:focus {
outline: none;
}
@@ -52,6 +54,11 @@
margin-left: -25px;
margin-top: 2px;
}
+
+ &.disabled {
+ opacity: 0.5;
+ background: none;
+ }
}
&.active {
> a, > button {
@@ -60,6 +67,10 @@
}
}
}
+.Dropdown-menu--top {
+ top: auto;
+ bottom: 100%;
+}
.Dropdown-menu--right {
left: auto;
right: 0;
diff --git a/less/lib/Form.less b/less/lib/Form.less
index c94a52c48..25a9d8606 100755
--- a/less/lib/Form.less
+++ b/less/lib/Form.less
@@ -13,3 +13,11 @@
.Form-group {
margin-bottom: 12px;
}
+
+.Form-group label {
+ font-size: 14px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ color: @text-color;
+ display: block;
+}
diff --git a/less/lib/Modal.less b/less/lib/Modal.less
index 45da92b69..0db85e8b7 100755
--- a/less/lib/Modal.less
+++ b/less/lib/Modal.less
@@ -101,11 +101,13 @@
color: @text-color;
}
- .helpText {
- font-size: 14px;
- line-height: 1.5em;
- margin-bottom: 25px;
- text-align: left;
+ .Form--centered {
+ .helpText {
+ font-size: 14px;
+ line-height: 1.5em;
+ margin-bottom: 25px;
+ text-align: left;
+ }
}
> :last-child {
diff --git a/less/lib/scaffolding.less b/less/lib/scaffolding.less
index 1ad846488..f1d0aa4ea 100755
--- a/less/lib/scaffolding.less
+++ b/less/lib/scaffolding.less
@@ -88,6 +88,7 @@ legend {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
+ color: @text-color;
}
input[type="search"] {
-webkit-appearance: none;
diff --git a/less/lib/variables.less b/less/lib/variables.less
index 4e3da2391..80fcead00 100755
--- a/less/lib/variables.less
+++ b/less/lib/variables.less
@@ -68,6 +68,9 @@
@alert-error-bg: #d83e3e;
@alert-error-color: #fff;
+@alert-success-bg: #B4F1AF;
+@alert-success-color: #33722D;
+
.define-header(@config-colored-header);
.define-header(false) {
@header-bg: @body-bg;
diff --git a/src/Admin/Actions/UpdateConfigAction.php b/src/Admin/Actions/UpdateConfigAction.php
new file mode 100644
index 000000000..31cb62d06
--- /dev/null
+++ b/src/Admin/Actions/UpdateConfigAction.php
@@ -0,0 +1,42 @@
+settings = $settings;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(Request $request, array $routeParams = [])
+ {
+ $config = array_get($request->getAttributes(), 'config', []);
+
+ // TODO: throw HTTP status 400 or 422
+ if (! is_array($config)) {
+ throw new Exception;
+ }
+
+ foreach ($config as $k => $v) {
+ $this->settings->set($k, $v);
+ }
+
+ return $this->success();
+ }
+}
diff --git a/src/Admin/Actions/UpdatePermissionAction.php b/src/Admin/Actions/UpdatePermissionAction.php
new file mode 100644
index 000000000..80379a19a
--- /dev/null
+++ b/src/Admin/Actions/UpdatePermissionAction.php
@@ -0,0 +1,29 @@
+getAttributes();
+ $permission = array_get($input, 'permission');
+ $groupIds = array_get($input, 'groupIds');
+
+ Permission::where('permission', $permission)->delete();
+
+ Permission::insert(array_map(function ($groupId) use ($permission) {
+ return [
+ 'permission' => $permission,
+ 'group_id' => $groupId
+ ];
+ }, $groupIds));
+
+ return $this->success();
+ }
+}
diff --git a/src/Admin/AdminServiceProvider.php b/src/Admin/AdminServiceProvider.php
index be60a09d3..a1fd767fb 100644
--- a/src/Admin/AdminServiceProvider.php
+++ b/src/Admin/AdminServiceProvider.php
@@ -50,6 +50,18 @@ class AdminServiceProvider extends ServiceProvider
'flarum.admin.index',
$this->action('Flarum\Admin\Actions\ClientAction')
);
+
+ $routes->post(
+ '/config',
+ 'flarum.admin.updateConfig',
+ $this->action('Flarum\Admin\Actions\UpdateConfigAction')
+ );
+
+ $routes->post(
+ '/permission',
+ 'flarum.admin.updatePermission',
+ $this->action('Flarum\Admin\Actions\UpdatePermissionAction')
+ );
}
protected function action($class)
diff --git a/src/Api/Serializers/ForumSerializer.php b/src/Api/Serializers/ForumSerializer.php
index 7210e4c83..b52eb3709 100644
--- a/src/Api/Serializers/ForumSerializer.php
+++ b/src/Api/Serializers/ForumSerializer.php
@@ -32,6 +32,7 @@ class ForumSerializer extends Serializer
];
if ($this->actor->isAdmin()) {
+ $attributes['adminUrl'] = Core::config('admin_url');
}
return $attributes;
diff --git a/src/Core/Settings/DatabaseSettingsRepository.php b/src/Core/Settings/DatabaseSettingsRepository.php
index ec726eefe..dd45a008c 100644
--- a/src/Core/Settings/DatabaseSettingsRepository.php
+++ b/src/Core/Settings/DatabaseSettingsRepository.php
@@ -29,6 +29,10 @@ class DatabaseSettingsRepository implements SettingsRepository
public function set($key, $value)
{
- $this->database->table('config')->where('key', $key)->update(['value' => $value]);
+ $query = $this->database->table('config')->where('key', $key);
+
+ $method = $query->exists() ? 'update' : 'insert';
+
+ $query->$method(compact('key', 'value'));
}
}