Acp redesign (#11639)

* acp sidebar

* gap in nav

* remove shadow

* label fixes

* color fixes

* feat: settings page wip

* feat: scroll spy 👓

move social into general, store social in meta.config like other settings
write upgrade script

* remove social

* rermove openapi routes

* cleanup, highlight selected nav item

* more cleanup

* advanced margin top

* derp

* match design

* bring back version alert

fix homepage js, since it moved to general settings

* remove unused tpls

these moved to general settings

* remove more  css

* offcanvas for mobile

fix search

* add timeout

* add new props

* manage categories

* small fixes

* category-edit

* feat category page fixes

* add title to settings pages

add user settings page

* small fixes

* some more settings pages

* fix: plugin page titles

* more settings pages

* more padding

* more pages, add acp paginator.tpl

so it doesn't change when active theme changes

* remove placeholder

* dashboard table

* fix: openapi

* fix: controller tests

* use fonts from core

* some small fixes

* fix rep page

* refactor: fix name of upgrade script

* create category modal

group edit

* group/groups pages

* admins mods

* privs

* uploads

* missing margin

* more acp pages

* more pages

* plugins/rewards/widgets

* wrap rewards

* fix widgets

* fix widget clone button

* fix group acp edit link

* update search dropdown

* remove display block from tbody

* use less css

* remove some derp links

* remove striped tables

* remove p tags from lang files

* update email settings

* Update api.tpl

* move tag-whitelist
isekai-main
Barış Soner Uşaklı 2 years ago committed by GitHub
parent 6d4ab1d0c1
commit c3afe44686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,6 +29,8 @@
},
"dependencies": {
"@adactive/bootstrap-tagsinput": "0.8.2",
"@fontsource/inter": "^4.5.14",
"@fontsource/poppins": "^4.5.10",
"@isaacs/ttlcache": "1.4.0",
"@popperjs/core": "2.11.8",
"ace-builds": "1.22.0",
@ -82,7 +84,6 @@
"lodash": "4.17.21",
"logrotate-stream": "0.2.9",
"lru-cache": "9.1.1",
"material-design-lite": "1.3.0",
"mime": "3.0.0",
"mkdirp": "3.0.1",
"mongodb": "5.5.0",

@ -4,6 +4,13 @@
"acp-title": "%1 | NodeBB Admin Control Panel",
"settings-header-contents": "Contents",
"changes-saved": "Changes Saved",
"changes-saved-message": "Your changes to the NodeBB configuration have been saved.",
"changes-not-saved": "Changes Not Saved",
"changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)"
"changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)",
"save-changes": "Save changes",
"min": "Min:",
"max": "Max:",
"view": "View",
"edit": "Edit"
}

@ -5,7 +5,7 @@
"uptime-seconds": "Uptime in Seconds",
"uptime-days": "Uptime in Days",
"mongo": "Mongo",
"mongo": "MongoDB",
"mongo.version": "MongoDB Version",
"mongo.storage-engine": "Storage Engine",
"mongo.collections": "Collections",

@ -26,13 +26,13 @@
"updates": "Updates",
"running-version": "You are running <strong>NodeBB v<span id=\"version\">%1</span></strong>.",
"keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.",
"up-to-date": "<p>You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i></p>",
"upgrade-available": "<p>A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>",
"prerelease-upgrade-available": "<p>This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.</p>",
"prerelease-warning": "<p>This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i></p>",
"up-to-date": "You are <strong>up-to-date</strong> <i class=\"fa fa-check\"></i>",
"upgrade-available": "A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.",
"prerelease-upgrade-available": "This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider <a href=\"https://docs.nodebb.org/configuring/upgrade/\" target=\"_blank\">upgrading your NodeBB</a>.",
"prerelease-warning": "This is a <strong>pre-release</strong> version of NodeBB. Unintended bugs may occur. <i class=\"fa fa-exclamation-triangle\"></i>",
"fallback-emailer-not-found": "Fallback emailer not found!",
"running-in-development": "<span>Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.</span>",
"latest-lookup-failed": "<p>Failed to look up latest available version of NodeBB</p>",
"running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator",
"latest-lookup-failed": "Failed to look up latest available version of NodeBB",
"notices": "Notices",
"restart-not-required": "Restart not required",

@ -1,4 +1,5 @@
{
"plugins": "Plugins",
"trending": "Trending",
"installed": "Installed",
"active": "Active",

@ -1,10 +1,12 @@
{
"rewards": "Rewards",
"add-reward": "Add reward",
"condition-if-users": "If User's",
"condition-is": "Is:",
"condition-then": "Then:",
"max-claims": "Amount of times reward is claimable",
"zero-infinite": "Enter 0 for infinite",
"select-reward": "Select reward",
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",

@ -1,4 +1,5 @@
{
"widgets": "Widgets",
"available": "Available Widgets",
"explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.",
"none-installed": "No widgets found! Activate the widget essentials plugin in the <a href=\"%1\">plugins</a> control panel.",

@ -1,10 +1,11 @@
{
"manage-admins-and-mods": "Manage Admins & Mods",
"administrators": "Administrators",
"global-moderators": "Global Moderators",
"moderators": "Moderators",
"no-global-moderators": "No Global Moderators",
"no-sub-categories": "No subcategories",
"subcategories": "%1 subcategories",
"view-children": "View children (%1)",
"no-moderators": "No Moderators",
"add-administrator": "Add Administrator",
"add-global-moderator": "Add Global Moderator",

@ -1,7 +1,11 @@
{
"manage-categories": "Manage Categories",
"add-category": "Add category",
"jump-to": "Jump to...",
"settings": "Category Settings",
"edit-category": "Edit Category",
"privileges": "Privileges",
"back-to-categories": "Back to categories",
"name": "Category Name",
"description": "Category Description",
"bg-color": "Background Colour",
@ -15,8 +19,11 @@
"post-queue": "Post queue",
"tag-whitelist": "Tag Whitelist",
"upload-image": "Upload Image",
"upload": "Upload",
"select-icon": "Select Icon",
"delete-image": "Remove",
"category-image": "Category Image",
"image-and-icon": "Image & Icon",
"parent-category": "Parent Category",
"optional-parent-category": "(Optional) Parent Category",
"top-level": "Top Level",
@ -31,6 +38,7 @@
"disable": "Disable",
"edit": "Edit",
"analytics": "Analytics",
"view-category": "View category",
"set-order": "Set order",
"set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.",

@ -1,4 +1,10 @@
{
"manage-groups": "Manage groups",
"add-group": "Add group",
"edit-group": "Edit Group",
"back-to-groups": "Back to groups",
"view-group": "View group",
"icon-and-title": "Icon & Title",
"name": "Group Name",
"badge": "Badge",
"properties": "Properties",
@ -10,7 +16,7 @@
"edit": "Edit",
"delete": "Delete",
"privileges": "Privileges",
"download-csv": "CSV",
"members-csv": "Members (CSV)",
"search-placeholder": "Search",
"create": "Create Group",
"description-placeholder": "A short description about your group",

@ -1,4 +1,6 @@
{
"manage-privileges": "Manage Privileges",
"discard-changes": "Discard changes",
"global": "Global",
"admin": "Admin",
"group-privileges": "Group Privileges",

@ -1,9 +1,11 @@
{
"manage-tags": "Manage Tags",
"none": "Your forum does not have any topics with tags yet.",
"bg-color": "Background Colour",
"text-color": "Text Colour",
"description": "Select tags by clicking or dragging, use <code>CTRL</code> to select multiple tags.",
"create": "Create Tag",
"add-tag": "Add tag",
"modify": "Modify Tags",
"rename": "Rename Tags",
"delete": "Delete Selected Tags",

@ -1,4 +1,5 @@
{
"manage-uploads": "Manage Uploads",
"upload-file": "Upload File",
"filename": "Filename",
"usage": "Post Usage",

@ -1,4 +1,5 @@
{
"manage-users": "Manage Users",
"users": "Users",
"edit": "Actions",
"make-admin": "Make Admin",

@ -72,7 +72,9 @@
"development/info": "Info",
"rebuild-and-restart-forum": "Rebuild & Restart Forum",
"rebuild-and-restart": "Rebuild & Restart",
"restart-forum": "Restart Forum",
"restart": "Restart",
"logout": "Log out",
"view-forum": "View Forum",

@ -1,11 +1,13 @@
{
"general-settings": "General Settings",
"on-this-page": "On this page:",
"site-settings": "Site Settings",
"title": "Site Title",
"title.short": "Short Title",
"title.short-placeholder": "If no short title is specified, the site title will be used",
"title.url": "Title Link URL",
"title.url-placeholder": "The URL of the site title",
"title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index. <br> Note: This is not the external URL used in emails, etc. That is set by the <code>url</code> property in config.json",
"title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index. Note: This is not the external URL used in emails, etc. That is set by the <code>url</code> property in config.json",
"title.name": "Your Community Name",
"title.show-in-header": "Show Site Title in Header",
"browser-title": "Browser Title",
@ -16,7 +18,7 @@
"description": "Site Description",
"keywords": "Site Keywords",
"keywords-placeholder": "Keywords describing your community, comma-separated",
"logo": "Site Logo",
"logo-and-icons": "Site Logo & Icons",
"logo.image": "Image",
"logo.image-placeholder": "Path to a logo to display on forum header",
"logo.upload": "Upload",

@ -1,5 +1,6 @@
{
"settings": "Settings",
"guest-settings": "Guest settings",
"handles.enabled": "Allow guest handles",
"handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"",
"topic-views.enabled": "Allow guests to increase topic view counts",

@ -1,4 +1,5 @@
{
"navigation": "Navigation",
"icon": "Icon:",
"change-icon": "change",
"route": "Route:",

@ -1,4 +1,5 @@
{
"general": "General",
"sorting": "Post Sorting",
"sorting.post-default": "Default Post Sorting",
"sorting.oldest-to-newest": "Oldest to Newest",
@ -23,10 +24,8 @@
"restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)",
"restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)",
"restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)",
"restrictions.min-title-length": "Minimum Title Length",
"restrictions.max-title-length": "Maximum Title Length",
"restrictions.min-post-length": "Minimum Post Length",
"restrictions.max-post-length": "Maximum Post Length",
"restrictions.title-length": "Title Length",
"restrictions.post-length": "Post Length",
"restrictions.days-until-stale": "Days until topic is considered stale",
"restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.",
"timestamp": "Timestamp",
@ -41,10 +40,9 @@
"teaser.last-reply": "Last &ndash; Show the latest reply, or a \"No replies\" placeholder if no replies",
"teaser.first": "First",
"showPostPreviewsOnHover": "Show a preview of posts when mouse overed",
"unread": "Unread Settings",
"unread-and-recent": "Unread & Recent Settings",
"unread.cutoff": "Unread cutoff days",
"unread.min-track-last": "Minimum posts in topic before tracking last read",
"recent": "Recent Settings",
"recent.max-topics": "Maximum topics on /recent",
"recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page",
"signature": "Signature Settings",

@ -27,5 +27,5 @@
"flags.action-on-resolve": "Do the following when a flag is resolved",
"flags.action-on-reject": "Do the following when a flag is rejected",
"flags.action.nothing": "Do nothing",
"flags.action.rescind": "Rescind the notification send to moderators/administrators"
"flags.action.rescind": "Rescind the notification sent to moderators/administrators"
}

@ -1,5 +1,4 @@
{
"post-sharing": "Post Sharing",
"info-plugins-additional": "Plugins can add additional networks for sharing posts.",
"save-success": "Successfully saved Post Sharing Networks!"
"info-plugins-additional": "Plugins can add additional networks for sharing posts."
}

@ -3,6 +3,7 @@
"link-to-manage": "Manage Tags",
"system-tags": "System Tags",
"system-tags-help": "Only privileged users will be able to use these tags.",
"tags-per-topic": "Tags per topic",
"min-per-topic": "Minimum Tags per Topic",
"max-per-topic": "Maximum Tags per Topic",
"min-length": "Minimum Tag Length",

@ -59,7 +59,7 @@
"max-about-me-length": "Maximum About Me Length",
"terms-of-use": "Forum Terms of Use <small>(Leave blank to disable)</small>",
"user-search": "User Search",
"user-search-results-per-page": "Number of results to display",
"user-search-results-per-page": "Number of users to display in search results",
"default-user-settings": "Default User Settings",
"show-email": "Show email",
"show-fullname": "Show fullname",

@ -24,6 +24,7 @@
"save_changes": "Save Changes",
"save": "Save",
"create": "Create",
"cancel": "Cancel",
"close": "Close",

@ -82,14 +82,8 @@ paths:
$ref: 'read/admin/dashboard/searches.yaml'
"/api/admin/settings/{term}":
$ref: 'read/admin/settings/term.yaml'
/api/admin/settings/languages:
$ref: 'read/admin/settings/languages.yaml'
/api/admin/settings/navigation:
$ref: 'read/admin/settings/navigation.yaml'
/api/admin/settings/homepage:
$ref: 'read/admin/settings/homepage.yaml'
/api/admin/settings/social:
$ref: 'read/admin/settings/social.yaml'
/api/admin/settings/api:
$ref: 'read/admin/settings/api.yaml'
/api/admin/settings/email:

@ -13,6 +13,8 @@ get:
properties:
categoriesPerPage:
type: number
selectCategoryLabel:
type: string
categoriesTree:
type: array
items:
@ -23,6 +25,8 @@ get:
description: A category identifier
name:
type: string
description:
type: string
disabled:
type: number
icon:

@ -11,6 +11,8 @@ get:
allOf:
- type: object
properties:
title:
type: string
groupsExemptFromMaintenanceMode:
type: array
items:

@ -11,6 +11,8 @@ get:
allOf:
- type: object
properties:
title:
type: string
tokens:
type: array
items:

@ -11,6 +11,8 @@ get:
allOf:
- type: object
properties:
title:
type: string
emails:
type: array
items:

@ -1,23 +0,0 @@
get:
tags:
- admin
summary: Get homepage settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
routes:
type: array
items:
type: object
properties:
route:
type: string
name:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -1,35 +0,0 @@
get:
tags:
- admin
summary: Get language settings
responses:
"200":
description: A JSON object containing available languages and settings
content:
application/json:
schema:
allOf:
- type: object
properties:
languages:
type: array
items:
type: object
properties:
name:
type: string
description: Localised name of the language
code:
type: string
description: A language code (similar to ISO-639)
dir:
type: string
description: Directionality of the language
enum: [ltr, rtl]
selected:
type: boolean
description: Denotes the currently selected default system language on the forum
autoDetectLang:
type: integer
description: Whether the forum will attempt to guess language based on browser's `Accept-Language` header
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -111,4 +111,6 @@ get:
navigation:
type: array
description: A clone of `enabled`
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -11,6 +11,8 @@ get:
allOf:
- type: object
properties:
title:
type: string
groupsExemptFromPostQueue:
type: array
items:

@ -1,28 +0,0 @@
get:
tags:
- admin
summary: Get post social sharing settings
responses:
"200":
description: "A JSON object containing post social sharing settings"
content:
application/json:
schema:
allOf:
- type: object
properties:
posts:
type: array
items:
type: object
properties:
id:
type: string
name:
type: string
class:
type: string
description: A FontAwesome icon string
activated:
type: boolean
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

@ -17,7 +17,17 @@ get:
schema:
allOf:
- type: object
properties: {}
properties:
title:
type: string
routes:
type: array
postSharing:
type: array
languages:
type: array
autoDetectLang:
type: number
additionalProperties:
type: object
description: Most of the settings pages have their values loaded on the client-side, so the settings are not exposed server-side.

@ -11,6 +11,8 @@ get:
allOf:
- type: object
properties:
title:
type: string
notificationSettings:
type: array
items:

@ -1,6 +1,8 @@
@import "./mixins";
@import "fonts";
@import "mixins";
@import "common";
@import "./header";
@import "sidebar";
@import "./mobile";
@import "./general/dashboard";
@ -12,10 +14,8 @@
@import "./settings/api";
@import "./appearance/customise";
@import "./extend/plugins";
@import "./extend/rewards";
@import "./extend/widgets";
@import "./advanced/database";
@import "./settings";
@import "settings";
@import "./modules/alerts";
@import "./modules/selectable";
@ -27,26 +27,29 @@ body {
}
.admin {
background: #fff;
font-size: 14px;
h1 {
font-size: 35px;
}
.form-label, .form-check-label {
font-weight: 700;
}
.btn {
border-radius: 0;
}
.btn-outline-secondary {
color: $gray-700;
box-shadow: 0 1px 4px rgba(0, 0, 0, .4) !important;
&:hover {
background-color: $gray-200 !important;
.acp-page-container {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: $spacer * 1.5;
padding: $spacer * 1.5;
padding-top: 0;
}
.acp-page-main-header {
background-color: white;
}
.settings, .categories, .category, .admins-mods {
hr {
color: $gray-500;
}
}
.form-control::placeholder, .bootstrap-tagsinput::placeholder {
color: $gray-500 !important;
}
// .floating-button can either be a container or the button itself
.floating-button {
position: fixed;
@ -73,22 +76,6 @@ body {
background: $primary !important;
}
.nodebb-logo {
img {
height: 31px;
margin-top: -8px;
margin-left: -7px;
vertical-align: -43%;
}
@include box-header-font;
color: #fff;
}
#breadcrumbs {
cursor: default;
}
@mixin acp-panel-heading() {
padding: 7px 14px;
border: 0;
@ -114,10 +101,6 @@ body {
}
}
.nav-header {
@include box-header-font;
}
.icon-container {
.fa-nbb-none {
border: 1px dotted black;
@ -141,27 +124,6 @@ body {
}
}
.navbar-static-top, .navbar-fixed-top {
box-shadow: 0px -3px 12px rgba(0, 0, 0, 0.5);
}
.navbar-header > .navbar-toggle {
margin-right: 8px;
}
.navbar-nav {
>li {
>#reconnect {
color: $gray-700;
}
>#reconnect:focus, >#reconnect:hover {
color: $gray-700;
background-color: transparent;
}
}
}
#taskbar {
display: none; /* not sure why I have to do this, but it only seems to show up on prod */
}
@ -172,57 +134,21 @@ body {
}
.bootstrap-tagsinput {
box-shadow: $input-box-shadow;
width: 100%;
border: 0;
box-shadow: none;
padding-left: 0;
input {
width: 100%;
margin-left: 1px;
margin-top: 9px;
border-bottom: 1px dotted #ccc !important;
padding-bottom: 5px;
padding-left: 0;
font-size: 0.875rem;
width: 64px;
padding: 0;
}
}
}
// Allowing text to the right of an image-type brand
// See: https://github.com/twbs/bootstrap/commit/8e2348e9eda51296eb680192379ab37f10355ca3
.navbar-brand > img {
display: inline-block;
}
.category-settings-form {
h3 {
margin-top: 0;
cursor: pointer;
}
h4 {
cursor: pointer;
}
}
.category-preview {
cursor: pointer;
width: 100%;
height: 100px;
text-align: center;
color: white;
margin-top: 0;
.icon {
width: 30px;
height: 30px;
line-height: 40px;
display: inline-block;
margin: 35px 5px 0 5px;
}
.dropdown-left .dropdown-menu {
--bs-position: start;
}
.category-dropdown-container.right .category-dropdown-menu {
.dropdown-right .dropdown-menu {
--bs-position: end;
}
@ -261,10 +187,6 @@ body {
opacity: 0.5;
}
form small {
color: $gray-700;
}
.caret {
display: inline-block;
width: 0;

@ -1,6 +0,0 @@
.database-info {
span {
display:inline-block;
width:220px;
}
}

@ -0,0 +1,73 @@
.form-label {
font-weight: 500;
font-size: $font-size-sm;
font-family: $font-family-base;
margin-bottom: 0;
}
.form-text {
font-size: 0.75rem!important;;
font-family: $font-family-base;
}
.tracking-tight { letter-spacing: -0.02em; }
$btn-ghost-hover-color: mix($light, $dark, 90%);
@mixin btn-ghost-base {
display: flex;
align-items: center;
justify-content: center;
gap: ($spacer * 0.5);
border-radius: $border-radius-sm;
border-width: 1px;
border-color: transparent;
background-color: transparent;
padding: ($spacer * 0.25) ($spacer * 0.5);
text-align: left;
--bs-text-opacity: 1;
color: rgb(73 80 87 / var(--bs-text-opacity));
font-family: $font-family-secondary;
cursor: pointer;
&:hover, &.active {
background-color: $btn-ghost-hover-color;
color: rgb(73 80 87 / var(--bs-text-opacity));
text-decoration: none;
}
}
.btn-ghost {
@include btn-ghost-base();
line-height: 1.5rem;
> i {
line-height: 1.5rem;
}
}
.btn-ghost-sm {
@include btn-ghost-base();
font-size: 0.875rem;
line-height: 1.25rem;
> i {
line-height: 1.25rem;
}
}
.btn-outline {
@include btn-ghost-base();
border-color: $border-color;
}
.btn-outline-sm {
@include btn-ghost-base();
border-color: $border-color;
font-size: 0.875rem;
line-height: 1.25rem;
}
.flex-basis-md-200 {
@include media-breakpoint-up(md) {
flex-basis: 200px!important;
}
}

@ -1,24 +1,4 @@
.plugins {
padding-left: 0px;
li {
list-style-type: none;
background: rgba(64, 64, 64, 0.05);
padding: 1em;
margin-bottom: 5px;
border-left: 5px solid #08c;
margin-left: -40px;
h2 {
font-size: 16px;
margin: 0;
}
p {
font-size: 12px;
}
}
.plugin-list.ui-sortable {
li {
cursor: pointer;

@ -1,52 +0,0 @@
#rewards {
ul {
list-style-type: none;
padding: 0px;
margin: 0px;
> li {
border-bottom: 1px solid #ddd;
margin-bottom: 20px;
&:last-child {
border-bottom: 0;
}
}
}
.rewards { width: 100%; }
.card {
border-radius: 2px;
border-width: 2px;
color: #333;
&.if-block {
border-color: $primary;
max-width: 33%;
}
&.this-block {
border-color: $warning;
max-width: 33%;
}
&.then-block {
border-color: $success;
max-width: 33%;
}
&.reward-block {
border-color: $success;
background-color: lighten($success, 15%);
color: #fff;
a, select, input { color: #fff; }
select > option { color: #333; }
width: 100%;
min-height: 110px;
}
}
}
.page-admin-rewards {
#new {
bottom: calc(64px + $spacer);
background-color: $success;
}
}

@ -0,0 +1,21 @@
@use "@fontsource/inter/scss/mixins" as Inter;
@use "@fontsource/poppins/scss/mixins" as Poppins;
$weights: $font-weight-light, $font-weight-normal, $font-weight-semibold, $font-weight-bold;
@each $weight in $weights {
@include Inter.fontFace(
$weight: $weight,
$display: fallback,
$fontDir: "./plugins/core/inter"
);
@include Poppins.fontFace(
$weight: $weight,
$display: fallback,
$fontDir: "./plugins/core/poppins"
);
}
.ff-base { font-family: $font-family-base !important; }
.ff-sans { font-family: $font-family-sans-serif !important; }
.ff-secondary { font-family: $font-family-secondary; }

@ -1,159 +0,0 @@
.header {
user-select: none;
position: relative;
background: #333;
width: 100%;
height: 200px;
margin-bottom: 50px;
font-size: 16px;
#main-page-title {
position: absolute;
left: 48px;
bottom: 50px;
color: #aaa;
font-size: 47px;
font-weight: 300;
}
.quick-actions {
position: static;
padding: 15px;
display: flex;
flex-direction: row-reverse;
margin: 0;
> * {
margin-right: 20px;
}
> .menu-button {
margin-right: 0;
padding: 0 5px;
}
.alert {
font-size: 14px;
margin-bottom: 0px;
&.alert-info {
background-color: #eee;
color: #333;
}
}
.dropdown {
margin-right: 0px;
.dropdown-toggle i {
padding: 0 1rem;
}
}
.fa {
line-height: 44px;
font-size: 25px;
}
#user_dropdown {
font-size: 25px;
color: #eee;
i {
margin-top: 12px;
display: block;
}
}
}
#acp-search {
input {
padding: 10px 20px;
width: 250px;
height: 44px;
background-color: rgba(0,0,0,.2);
border-radius: 3px;
box-shadow: none;
transition: .4s ease background-color;
&:focus {
background-color: #eee;
color: #333;
}
}
.dropdown:not(.open) {
&:before {
content: '/';
border: 1px solid $gray-500;
border-radius: 5px;
padding: 0px 6px;
font-size: 12px;
font-weight: 600;
pointer-events: none;
position: absolute;
top: 13px;
left: 1em;
}
&:after {
content: attr(data-text);
position: absolute;
top: 13px;
left: 3em;
font-size: small;
font-weight: 600;
pointer-events: none;
}
input {
color: transparent;
}
}
.search-match {
font-weight: 700;
color: black;
}
}
#main-menu > li {
padding-bottom: 10px;
}
> ul {
list-style-type: none;
padding: 0px;
position: absolute;
bottom: -16px;
left: 50px;
> li {
float: left;
margin-right: 30px;
border-bottom: 4px solid transparent;
transition: border-color 150ms linear;
&:hover {
border-color: darken($primary, 20%);
}
&.active {
border-color: $primary;
}
> a {
color: white;
text-transform: uppercase;
text-decoration: none;
outline: none;
}
}
}
.plugins-menu {
max-height: 50vh;
overflow-y: auto;
}
}

@ -1,51 +1,20 @@
div.categories {
ul[data-cid] {
user-select: none;
list-style-type: none;
margin: 0;
padding: 0;
> li > ul > li {
margin-left: 4.5rem;
}
> li > a {
margin-left: 4.5rem;
}
.row {
margin-left: -15px;
margin-right: -15px;
padding-left: 3rem;
}
> li li:last-child {
.row {
border-bottom: 0px;
}
}
> li {
margin: 16px 0 24px 0;
&.placeholder {
border: 1px dashed #2196F3;
background-color: #E1F5FE;
width: 100%;
}
}
}
.stats {
display: inline-block;
li {
min-height: 0;
display: inline;
margin: 0 16px 0 0;
left: 0;
}
}
.disabled > .category-row {
.icon, .category-header, .description {
.icon, .title, .description {
opacity: 0.5;
}
@ -57,34 +26,15 @@ div.categories {
.toggle {
width: 24px;
height: 24px;
border-radius: 50%;
line-height: 24px;
text-align: center;
vertical-align: bottom;
background-size: cover;
float: left;
margin-right: 0px;
cursor: pointer;
.fa {
font-size: 85%;
}
}
.information {
cursor: move;
padding-left: 3rem;
}
.category-header {
margin-top: 0;
margin-bottom: 8px;
}
.description {
margin: 0;
}
.children-placeholder{
.children-placeholder {
min-height: 20px;
height: 20px;
}

@ -1,30 +1,5 @@
.group {
[component="groups/members"] {
padding: 0;
tbody {
max-height: 500px;
display: block;
overflow-y: auto;
.member-name {
width: 100%;
}
}
}
#group-icon {
cursor: pointer;
}
}
.groups {
#group-search {
margin-bottom: 10px;
}
.groups-list {
p {
margin: 0;
}
td {
max-width: 350px;
}

@ -1,22 +1,12 @@
.tags {
.tag-list {
h3 {
min-width: 225px;
}
.tag-row {
padding: 0.5rem;
float: left;
margin-left: 0.5rem;
.tag-item {
cursor: pointer;
display: inline-block;
font-size: 11px;
}
&.ui-selected {
background: lighten($success, 25%);
border-radius: $border-radius-sm;
}
&.ui-selecting {
@ -24,8 +14,4 @@
}
}
}
.tag-topic-count {
font-size: 14px;
}
}

@ -1,191 +1,12 @@
#mobile-menu {
display: none;
}
@media (max-width: 991px) {
body {
height: 100%;
}
#panel {
background-color: inherit;
min-height: 100%;
}
body, #panel, .slideout-menu {
-webkit-overflow-scrolling: touch;
}
.header {
height: 58px;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.26);
position: fixed;
top: 0px;
z-index: 5;
#main-page-title {
bottom: -31px;
font-size: 20px;
color: #FFF;
left: 52px;
font-weight: 400;
}
#main-menu {
display: none;
}
}
#mobile-menu {
width: 22px;
background: none;
border: none;
margin-right: 10px;
margin-left: -5px;
outline: none !important;
display: block;
position: absolute;
top: 22px;
left: 22px;
.bar {
width: 100%;
height: 2px;
background: #fff;
margin-bottom: 3px;
border-radius: 10px;
}
}
#menu {
background-color: #1D1F20;
background-image: linear-gradient(145deg, #1D1F20, #404348);
a {
color: #fff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
}
.menu-header-title {
font-weight: 400;
letter-spacing: 0.5px;
margin: 0;
}
.menu-section {
margin: 25px 0;
&.quick-actions {
margin: 0;
.button-group {
display: flex;
justify-content: center;
}
.alert {
border-radius: 0;
.span {
display: block;
}
}
}
}
.menu-section-title {
text-transform: uppercase;
color: #85888d;
font-weight: 200;
font-size: 13px;
letter-spacing: 1px;
padding: 0 20px;
margin:0;
}
.menu-section-list {
padding: 0;
margin: 10px 0;
list-style: none;
a {
display: block;
padding: 10px 20px;
}
a:hover {
background-color: rgba(255, 255, 255, 0.1);
text-decoration: none;
}
}
#panel {
background: white;
min-height: 100%;
padding-top: 80px;
}
.slideout-menu {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 0;
width: 256px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
display: none;
}
.slideout-panel {
position: relative;
}
.slideout-open,
.slideout-open body,
.slideout-open .slideout-panel {
overflow: hidden;
overflow-y: hidden !important;
}
.slideout-open .slideout-menu {
display: block;
overflow-y: scroll;
overflow-x: hidden;
}
html {
height: 100%;
overflow-y: hidden;
}
.slideout-open {
overflow-y: hidden;
height: 100%;
}
body {
overflow-y: scroll;
overflow-x: hidden;
}
}
@media (max-width: 768px) {
.content-header, .settings-header {
font-size: 200%;
margin-bottom: 20px;
margin-left: -2px;
}
.dropdown-menu {
margin-top: -35px;
margin-right: -2px;
}
}

@ -1,12 +1,9 @@
#acp-search {
.acp-search {
.dropdown-menu {
max-height: 75vh;
overflow-y: auto;
> li > a {
// &.focus {
// &:extend(.dropdown-menu>li>a:focus);
// }
&:focus {
outline: none;
}

@ -0,0 +1,62 @@
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #e9ecef !default;
$gray-300: #dee2e6 !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #6c757d !default;
$gray-700: #495057 !default;
$gray-800: #343a40 !default;
$gray-900: #212529 !default;
$black: #000 !default;
$blue: #0d6efd !default;
$red: #dc3545 !default;
$yellow: #ffc107 !default;
$green: #198754 !default;
$cyan: #0dcaf0 !default;
$body-color: $gray-800;
$text-muted: $gray-600 !default;
// Custom fonts
$font-family-sans-serif: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$font-family-secondary: "Poppins", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
$font-weight-semibold: 500 !default;
$font-size-base: 1rem !default;
$link-decoration: none;
$link-hover-decoration: underline;
// options
$enable-gradients: false;
// no caret on dropdown-toggle
$enable-caret: false;
// disable smooth scroll, this makes window.scrollTo(0,0) in ajaxify.js take x milliseconds
$enable-smooth-scroll: false;
$enable-shadows: true;
// Buttons
$input-btn-padding-y: .4rem !default;
$input-btn-padding-x: 1rem !default;
// Forms
$input-padding-y: 0.5rem !default;
$input-padding-x: 0.75rem !default;
// $input-padding-y-sm: 0 !default;
// $input-padding-x-sm: 0 !default;
// $input-padding-y-lg: ($font-size-base * 1.25) !default;
// $input-padding-x-lg: 0 !default;
$input-btn-focus-color: #c29ffa80 !default;
$input-border-radius: 0.25rem !default;
$input-focus-border-color: #c29ffa80 !default;
// change inset shadow to outset
$box-shadow-inset: 0 1px 2px rgba($black, .075) !default;

@ -1,20 +0,0 @@
.settings {
.section-content {
border-left: 3px solid $primary;
ul {
list-style-type: none;
font-size: 16px;
padding-left: 20px;
li:not(:last-child) {
margin-bottom: 5px;
}
a {
text-decoration: none;
&:hover{
text-decoration: underline;
}
}
}
}
}

@ -0,0 +1,13 @@
#sidebar-left, #offcanvas {
.btn-ghost, .btn-ghost-sm {
i {
color: $gray-500;
}
}
.accordion-body {
.btn-ghost-sm {
padding-left: 38px!important;
}
}
}

@ -1,45 +0,0 @@
// system font family
// based on those in [bootstrap@5.0.0-alpha1](https://github.com/twbs/bootstrap/blob/b531bda07cbea2e124194aefe3b8597b3ac2578e/scss/_variables.scss#L386)
// and [wordpress admin](https://core.trac.wordpress.org/browser/trunk/src/wp-admin/css/common.css?rev=47835#L220)
// system-ui : supported by the latest browsers for this very purpose
// apple-system, BlinkMacSystemFont : iOS and MacOS
// "Segoe UI" : Windows Vista, 7, 8, 10
// Roboto : Android 4.0+
// Oxygen-Sans : KDE
// Ubuntu : Ubuntu
// Cantarell : GNOME
// "Helvetica Neue" : Mac OS X
// Helvetica : backup, better looking than Arial
// Arial : backup
// "Noto Sans" : broader language support on Android
// sans-serif : whatever the browser can give us
// "Apple Color Emoji" : Emoji on iOS and MacOS
// "Segoe UI Emoji", "Segoe UI Symbol" : Emoji on Windows
// "Noto Color Emoji" : Emoji on Android
$font-family-system: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$font-family-sans-serif: $font-family-system;
$font-size-base: 0.875rem!default;
$body-color: #666!default;
$light: #f5f5f5;
// options
$enable-gradients: false;
// no caret on dropdown-toggle
$enable-caret: false;
$font-size-base: 1rem !default;
$font-weight-base: 400 !default;
// Buttons
$input-btn-padding-y: .4rem !default;
$input-btn-padding-x: 1rem !default;
// Forms
$input-padding-y: 0.4rem !default;
$input-padding-x: 0 !default;
$input-padding-y-sm: 0 !default;
$input-padding-x-sm: 0 !default;
$input-padding-y-lg: ($font-size-base * 1.25) !default;
$input-padding-x-lg: 0 !default;

@ -42,9 +42,17 @@ app.onDomReady();
}
require(['hooks'], (hooks) => {
hooks.on('action:ajaxify.end', () => {
hooks.on('action:ajaxify.end', (data) => {
updatePageTitle(data.url);
setupRestartLinks();
showCorrectNavTab();
startLogoutTimer();
$('[data-bs-toggle="tooltip"]').tooltip({
animation: false,
container: '#content',
});
if ($('.settings').length) {
require(['admin/settings'], function (Settings) {
Settings.prepare();
@ -52,21 +60,35 @@ app.onDomReady();
});
}
});
hooks.on('action:ajaxify.start', function () {
require(['bootstrap'], function (boostrap) {
const offcanvas = boostrap.Offcanvas.getInstance('#offcanvas');
if (offcanvas) {
offcanvas.hide();
}
});
});
});
function showCorrectNavTab() {
// show correct tab if url has #
if (window.location.hash) {
$('.nav-pills a[href="' + window.location.hash + '"]').tab('show');
const accordionEl = $('[component="acp/accordion"]');
let pathname = window.location.pathname;
if (pathname === '/admin') {
pathname = '/admin/dashboard';
}
const selectedButton = accordionEl.find(`a[href="${pathname}"]`);
if (selectedButton.length) {
accordionEl.find('a').removeClass('active');
accordionEl.find('.accordion-collapse').removeClass('show');
selectedButton.addClass('active');
selectedButton.parents('.accordion-collapse').addClass('show');
}
}
$(document).ready(function () {
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
require(['admin/modules/search'], function (search) {
search.init();
});
}
require(['admin/modules/search'], function (search) {
search.init();
});
$('[component="logout"]').on('click', function () {
require(['logout'], function (logout) {
@ -75,16 +97,25 @@ app.onDomReady();
return false;
});
configureSlidemenu();
setupNProgress();
fixAccordionIds();
});
$(window).on('action:ajaxify.contentLoaded', function (ev, data) {
selectMenuItem(data.url);
setupRestartLinks();
require('material-design-lite');
componentHandler.upgradeDom();
});
function fixAccordionIds() {
// fix mobile accordion, so it doesn't have same ids as desktop
// the same accordion partial is used in both places
const offcanvasAccordion = $('#offcanvas #accordionACP');
offcanvasAccordion.attr('id', 'accordionACP-offcanvas');
offcanvasAccordion.find('[data-bs-target]').each((i, el) => {
$(el).attr('data-bs-target', $(el).attr('data-bs-target') + '-offcanvas');
});
offcanvasAccordion.find('[data-bs-parent]').each((i, el) => {
$(el).attr('data-bs-parent', '#accordionACP-offcanvas');
});
offcanvasAccordion.find('.accordion-collapse').each((i, el) => {
$(el).attr('id', $(el).attr('id') + '-offcanvas');
});
}
function setupNProgress() {
require(['nprogress', 'hooks'], function (NProgress, hooks) {
@ -98,7 +129,7 @@ app.onDomReady();
});
}
function selectMenuItem(url) {
function updatePageTitle(url) {
require(['translator'], function (translator) {
url = url
.replace(/\/\d+$/, '')
@ -113,17 +144,8 @@ app.onDomReady();
url = [config.relative_path, url].join('/');
let fallback;
$('#main-menu li').removeClass('active');
$('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () {
const menu = $(this);
if (menu.parent().attr('data-link')) {
return;
}
menu
.parent().addClass('active')
.parents('.menu-item').addClass('active');
fallback = menu.text();
$(`[component="acp/accordion"] a[href="${url}"]`).each(function () {
fallback = $(this).text();
});
let mainTitle;
@ -152,9 +174,6 @@ app.onDomReady();
translator.translate(pageTitle, function (title) {
document.title = title.replace(/&gt;/g, '>');
});
translator.translate(mainTitle, function (text) {
$('#main-page-title').text(text);
});
});
}
@ -164,7 +183,7 @@ app.onDomReady();
// otherwise it can be unloaded when rebuild & restart is run
// the client can't fetch the template file, resulting in an error
benchpress.render('partials/toast', {}).then(function () {
$('.rebuild-and-restart').off('click').on('click', function () {
$('[component="rebuild-and-restart"]').off('click').on('click', function () {
bootbox.confirm('[[admin/admin:alert.confirm-rebuild-and-restart]]', function (confirm) {
if (confirm) {
instance.rebuildAndRestart();
@ -172,7 +191,7 @@ app.onDomReady();
});
});
$('.restart').off('click').on('click', function () {
$('[component="restart"]').off('click').on('click', function () {
bootbox.confirm('[[admin/admin:alert.confirm-restart]]', function (confirm) {
if (confirm) {
instance.restart();
@ -182,62 +201,4 @@ app.onDomReady();
});
});
}
function configureSlidemenu() {
require(['slideout'], function (Slideout) {
let env = utils.findBootstrapEnvironment();
const slideout = new Slideout({
panel: document.getElementById('panel'),
menu: document.getElementById('menu'),
padding: 256,
tolerance: 70,
});
if (env === 'md' || env === 'lg') {
slideout.disableTouch();
}
$('#mobile-menu').on('click', function () {
slideout.toggle();
});
$('#menu a').on('click', function () {
slideout.close();
});
$(window).on('resize', function () {
slideout.close();
env = utils.findBootstrapEnvironment();
if (['xxl', 'xl', 'lg'].includes(env)) {
slideout.disableTouch();
$('#header').css({
position: 'relative',
});
} else {
slideout.enableTouch();
$('#header').css({
position: 'fixed',
});
}
});
function onOpeningMenu() {
$('#header').css({
top: ($('#panel').position().top * -1) + 'px',
position: 'absolute',
});
}
slideout.on('open', onOpeningMenu);
slideout.on('close', function () {
$('#header').css({
top: '0px',
position: 'fixed',
});
});
});
}
}());

@ -43,10 +43,6 @@ define('admin/dashboard', [
isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
$('[data-bs-toggle="tooltip"]').tooltip({
animation: false,
});
setupRealtimeButton();
setupGraphs(function () {
socket.emit('admin.rooms.getAll', Admin.updateRoomUsage);
@ -68,19 +64,19 @@ define('admin/dashboard', [
const html = '<div class="text-center float-start">' +
'<span class="formatted-number">' + data.onlineRegisteredCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.users]]</div>' +
'<div class="stat text-nowrap">[[admin/dashboard:active-users.users]]</div>' +
'</div>' +
'<div class="text-center float-start">' +
'<span class="formatted-number">' + data.onlineGuestCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.guests]]</div>' +
'<div class="stat text-nowrap">[[admin/dashboard:active-users.guests]]</div>' +
'</div>' +
'<div class="text-center float-start">' +
'<span class="formatted-number">' + (data.onlineRegisteredCount + data.onlineGuestCount) + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.total]]</div>' +
'<div class="stat text-nowrap">[[admin/dashboard:active-users.total]]</div>' +
'</div>' +
'<div class="text-center float-start">' +
'<span class="formatted-number">' + data.socketCount + '</span>' +
'<div class="stat">[[admin/dashboard:active-users.connections]]</div>' +
'<div class="stat text-nowrap">[[admin/dashboard:active-users.connections]]</div>' +
'</div>';
updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount);

@ -31,7 +31,8 @@ define('admin/extend/plugins', [
pluginsList.on('click', 'button[data-action="toggleActive"]', function () {
const pluginEl = $(this).parents('li');
pluginID = pluginEl.attr('data-plugin-id');
const btn = $('[id="' + pluginID + '"] [data-action="toggleActive"]');
// const btn = $('[id="' + pluginID + '"] [data-action="toggleActive"]');
const btn = $(this);
const pluginData = ajaxify.data.installed[pluginEl.attr('data-plugin-index')];
@ -40,30 +41,28 @@ define('admin/extend/plugins', [
if (err) {
return alerts.error(err);
}
translator.translate('<i class="fa fa-power-off"></i> [[admin/extend/plugins:plugin-item.' + (status.active ? 'deactivate' : 'activate') + ']]', function (buttonText) {
btn.html(buttonText);
btn.toggleClass('btn-warning', status.active).toggleClass('btn-success', !status.active);
btn.siblings('[data-action="toggleActive"]').removeClass('hidden');
btn.addClass('hidden');
// clone it to active plugins tab
if (status.active && !$('#active [id="' + pluginID + '"]').length) {
$('#active ul').prepend(pluginEl.clone(true));
}
// clone it to active plugins tab
if (status.active && !$('#active [id="' + pluginID + '"]').length) {
$('#active ul').prepend(pluginEl.clone(true));
}
// Toggle active state in template data
pluginData.active = !pluginData.active;
alerts.alert({
alert_id: 'plugin_toggled',
title: '[[admin/extend/plugins:alert.' + (status.active ? 'enabled' : 'disabled') + ']]',
message: '[[admin/extend/plugins:alert.' + (status.active ? 'activate-success' : 'deactivate-success') + ']]',
type: status.active ? 'warning' : 'success',
timeout: 5000,
clickfn: function () {
require(['admin/modules/instance'], function (instance) {
instance.rebuildAndRestart();
});
},
});
// Toggle active state in template data
pluginData.active = !pluginData.active;
alerts.alert({
alert_id: 'plugin_toggled',
title: '[[admin/extend/plugins:alert.' + (status.active ? 'enabled' : 'disabled') + ']]',
message: '[[admin/extend/plugins:alert.' + (status.active ? 'activate-success' : 'deactivate-success') + ']]',
type: status.active ? 'warning' : 'success',
timeout: 5000,
clickfn: function () {
require(['admin/modules/instance'], function (instance) {
instance.rebuildAndRestart();
});
},
});
});
}

@ -95,16 +95,16 @@ define('admin/extend/rewards', ['alerts'], function (alerts) {
html += '<label for="' + input.name + '">' + input.label + '<br />';
switch (input.type) {
case 'select':
html += '<select class="form-control" name="' + input.name + '">';
html += '<select class="form-select form-select-sm" name="' + input.name + '">';
input.values.forEach(function (value) {
html += '<option value="' + value.value + '">' + value.name + '</option>';
});
break;
case 'text':
html += '<input type="text" class="form-control" name="' + input.name + '" />';
html += '<input type="text" class="form-control form-control-sm" name="' + input.name + '" />';
break;
}
html += '</label><br />';
html += '</label>';
});
div.html(html);

@ -15,12 +15,14 @@ define('admin/manage/categories', [
let sortables;
Categories.init = function () {
categorySelector.init($('.category [component="category-selector"]'), {
categorySelector.init($('[component="category-selector"]'), {
parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0,
onSelect: function (selectedCategory) {
ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : ''));
},
cacheList: false,
localCategories: [],
template: 'admin/partials/category/selector-dropdown-right',
});
Categories.render(ajaxify.data.categoriesTree);
@ -74,12 +76,14 @@ define('admin/manage/categories', [
});
});
$('#collapse-all').on('click', function () {
toggleAll(false);
});
$('#expand-all').on('click', function () {
toggleAll(true);
$('#toggle-collapse-all').on('click', function () {
const $this = $(this);
const isCollapsed = parseInt($this.attr('data-collapsed'), 10) === 1;
toggleAll(isCollapsed);
$this.attr('data-collapsed', isCollapsed ? 0 : 1)
.translateText(isCollapsed ?
'[[admin/manage/categories:collapse-all]]' :
'[[admin/manage/categories:expand-all]]');
});
function toggleAll(expand) {
@ -96,7 +100,7 @@ define('admin/manage/categories', [
message: html,
buttons: {
save: {
label: '[[global:save]]',
label: '[[global:create]]',
className: 'btn-primary',
callback: submit,
},
@ -110,6 +114,7 @@ define('admin/manage/categories', [
icon: 'fa-none',
},
],
template: 'admin/partials/category/selector-dropdown-left',
};
const parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options);
const cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options);
@ -177,7 +182,7 @@ define('admin/manage/categories', [
};
Categories.toggle = function (cids, disabled) {
const listEl = document.querySelector('.categories ul');
const listEl = document.querySelector('.categories [data-cid="0"]');
Promise.all(cids.map(cid => api.put('/categories/' + cid, {
disabled: disabled ? 1 : 0,
}).then(() => {
@ -214,14 +219,14 @@ define('admin/manage/categories', [
if (oldParentCid !== newParentCid) {
const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`);
if (toggle) {
toggle.classList.toggle('hide', false);
toggle.classList.toggle('invisible', false);
}
const children = document.querySelectorAll(`.categories li[data-cid="${oldParentCid}"] ul[data-cid] li[data-cid]`);
if (!children.length) {
const toggle = document.querySelector(`.categories li[data-cid="${oldParentCid}"] .toggle`);
if (toggle) {
toggle.classList.toggle('hide', true);
toggle.classList.toggle('invisible', true);
}
}
@ -278,7 +283,7 @@ define('admin/manage/categories', [
// Disable expand toggle
if (!categories.length) {
const toggleEl = container.get(0).querySelector('.toggle');
toggleEl.classList.toggle('hide', true);
toggleEl.classList.toggle('invisible', true);
}
// Handle and children categories in this level have

@ -14,23 +14,51 @@ define('admin/manage/category', [
let updateHash = {};
Category.init = function () {
$('#category-settings select').each(function () {
const categorySettings = $('#category-settings');
const previewEl = $('[component="category/preview"]');
categorySettings.find('select').each(function () {
const $this = $(this);
$this.val($this.attr('data-value'));
});
categorySelector.init($('[component="category-selector"]'), {
// category switcher
categorySelector.init($('[component="settings/main/header"] [component="category-selector"]'), {
onSelect: function (selectedCategory) {
ajaxify.go('admin/manage/categories/' + selectedCategory.cid);
},
cacheList: false,
showLinks: true,
template: 'admin/partials/category/selector-dropdown-right',
});
// parent selector
categorySelector.init($('#parent-category-selector [component="category-selector"]'), {
onSelect: function (selectedCategory) {
const parentCidInput = $('#parent-cid');
parentCidInput.val(selectedCategory.cid);
modified(parentCidInput[0]);
},
selectedCategory: ajaxify.data.category.parent, // switch selection to parent
localCategories: [
{
cid: 0,
name: '[[admin/manage/categories:parent-category-none]]',
icon: 'fa-list',
},
],
cacheList: false,
showLinks: true,
template: 'admin/partials/category/selector-dropdown-right',
});
handleTags();
$('#category-settings input, #category-settings select, #category-settings textarea').on('change', function (ev) {
categorySettings.find('input, select, textarea').on('change', function (ev) {
modified(ev.target);
});
$('[type="checkbox"]').on('change', function () {
modified($(this));
});
$('[data-name="imageClass"]').on('change', function () {
$('.category-preview').css('background-size', $(this).val());
@ -38,7 +66,6 @@ define('admin/manage/category', [
$('[data-name="bgColor"], [data-name="color"]').on('input', function () {
const $inputEl = $(this);
const previewEl = $inputEl.parents('[data-cid]').find('.category-preview');
if ($inputEl.attr('data-name') === 'bgColor') {
previewEl.css('background-color', $inputEl.val());
} else if ($inputEl.attr('data-name') === 'color') {
@ -171,56 +198,41 @@ define('admin/manage/category', [
params: { cid: cid },
}, function (imageUrlOnServer) {
$('#category-image').val(imageUrlOnServer);
const previewBox = inputEl.parent().parent().siblings('.category-preview');
previewBox.css('background', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')');
previewEl.css('background-image', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')');
modified($('#category-image'));
});
});
$('#category-image').on('change', function () {
$('.category-preview').css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : '');
previewEl.css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : '');
modified($('#category-image'));
});
$('.delete-image').on('click', function (e) {
e.preventDefault();
const inputEl = $('#category-image');
const previewBox = $('.category-preview');
inputEl.val('');
previewBox.css('background-image', '');
previewEl.css('background-image', '');
modified(inputEl[0]);
$(this).parent().addClass('hide').hide();
});
$('.category-preview').on('click', function () {
previewEl.on('click', function () {
iconSelect.init($(this).find('i'), modified);
});
$('[type="checkbox"]').on('change', function () {
modified($(this));
});
$('button[data-action="setParent"], button[data-action="changeParent"]').on('click', Category.launchParentSelector);
$('button[data-action="removeParent"]').on('click', function () {
api.put('/categories/' + ajaxify.data.category.cid, {
parentCid: 0,
}).then(() => {
$('button[data-action="removeParent"]').parent().addClass('hide');
$('button[data-action="changeParent"]').parent().addClass('hide');
$('button[data-action="setParent"]').removeClass('hide');
}).catch(alerts.error);
});
$('button[data-action="toggle"]').on('click', function () {
const $this = $(this);
const disabled = $this.attr('data-disabled') === '1';
api.put('/categories/' + ajaxify.data.category.cid, {
disabled: disabled ? 0 : 1,
}).then(() => {
$this.translateText(!disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]');
$this.toggleClass('btn-primary', !disabled).toggleClass('btn-danger', disabled);
$this.find('.label').translateText(
!disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]'
);
$this.find('i')
.toggleClass(['fa-check', 'text-success'], !disabled)
.toggleClass(['fa-ban', 'text-danger'], disabled);
$this.attr('data-disabled', disabled ? 0 : 1);
}).catch(alerts.error);
});
@ -276,30 +288,5 @@ define('admin/manage/category', [
});
}
Category.launchParentSelector = function () {
categorySelector.modal({
onSubmit: function (selectedCategory) {
const parentCid = selectedCategory.cid;
if (!parentCid) {
return;
}
api.put('/categories/' + ajaxify.data.category.cid, {
parentCid: parentCid,
}).then(() => {
api.get(`/categories/${parentCid}`, {}).then(function (parent) {
if (parent && parent.icon && parent.name) {
const buttonHtml = '<i class="fa ' + parent.icon + '"></i> ' + parent.name;
$('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide');
}
});
$('button[data-action="removeParent"]').parent().removeClass('hide');
$('button[data-action="setParent"]').addClass('hide');
}).catch(alerts.error);
},
showLinks: true,
});
};
return Category;
});

@ -47,7 +47,7 @@ define('admin/manage/group', [
setupGroupMembersMenu();
$('#group-icon, #group-icon-label').on('click', function () {
$('#group-icon-container').on('click', function () {
const currentIcon = groupIcon.attr('value');
iconSelect.init(groupIcon, function () {
let newIcon = groupIcon.attr('value');
@ -77,6 +77,7 @@ define('admin/manage/group', [
cids = cids.filter((cid, index, array) => array.indexOf(cid) === index);
$('#memberPostCids').val(cids.join(','));
cidSelector.selectCategory(0);
return false;
},
});
@ -87,6 +88,16 @@ define('admin/manage/group', [
app.flags._unsaved = true;
});
$('[data-action="delete"]').on('click', function () {
bootbox.confirm('[[admin/manage/groups:alerts.confirm-delete]]', function (confirm) {
if (confirm) {
api.del(`/groups/${slugify(ajaxify.data.group.name)}`, {}).then(() => {
ajaxify.go('/admin/managegroups');
}).catch(alerts.error);
}
});
});
$('#save').on('click', function () {
api.put(`/groups/${slugify(groupName)}`, {
name: $('#change-group-name').val(),

@ -30,22 +30,8 @@ define('admin/manage/groups', [
break;
}
});
enableCategorySelectors();
};
function enableCategorySelectors() {
$('.groups-list [component="category-selector"]').each(function () {
const nameEncoded = $(this).parents('[data-name-encoded]').attr('data-name-encoded');
categorySelector.init($(this), {
onSelect: function (selectedCategory) {
ajaxify.go('admin/manage/privileges/' + selectedCategory.cid + '?group=' + nameEncoded);
},
showLinks: true,
});
});
}
function handleCreate() {
$('#create').on('click', function () {
app.parseAndTranslate('admin/partials/create_group_modal', {}).then((html) => {
@ -119,7 +105,6 @@ define('admin/manage/groups', [
}, function (html) {
groupsEl.find('[data-groupname]').remove();
groupsEl.find('tbody').append(html);
enableCategorySelectors();
});
});
}

@ -21,7 +21,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
// and wrap the match in a `.search-match` element
.replace(
new RegExp('^[\\s\\S]*?(.{0,25})(' + escaped + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
'...$1<span class="search-match">$2</span>$3...<br>'
'...$1<span class="search-match fw-bold">$2</span>$3...<br>'
)
// collapse whitespace
.replace(/(?:\n ?)+/g, '\n')
@ -33,7 +33,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
);
return '<li role="presentation" class="result">' +
'<a class="dropdown-item" role="menuitem" href= "' + config.relative_path + '/' + namespace + '" >' +
'<a class="dropdown-item rounded-1" role="menuitem" href= "' + config.relative_path + '/' + namespace + '" >' +
title +
'<br>' + (!results ? '' :
('<small><code>' +
@ -60,11 +60,17 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
};
function setupACPSearch(dict) {
const dropdown = $('#acp-search .dropdown');
const menu = $('#acp-search .dropdown-menu');
const input = $('#acp-search input');
const placeholderText = dropdown.attr('data-text');
const searchEls = $('[component="acp/search"]');
searchEls.each((index, searchEl) => {
setupSearch(dict, $(searchEl));
});
}
function setupSearch(dict, searchEl) {
const dropdown = searchEl.find('.dropdown');
const menu = searchEl.find('.dropdown-menu');
const input = searchEl.find('input');
const placeholderText = dropdown.attr('data-text');
if (!config.searchEnabled) {
menu.addClass('search-disabled');
}
@ -73,7 +79,7 @@ define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, ale
dropdown.addClass('open');
});
$('#acp-search').parents('form').on('submit', function (ev) {
searchEl.parents('form').on('submit', function (ev) {
const query = input.val();
const selected = menu.get(0).querySelector('li.result > a.focus') || menu.get(0).querySelector('li.result > a');
const href = selected ? selected.getAttribute('href') : config.relative_path + '/search?in=titlesposts&term=' + escape(query);

@ -1,29 +1,55 @@
'use strict';
define('admin/settings', ['uploader', 'mousetrap', 'hooks', 'alerts', 'settings'], function (uploader, mousetrap, hooks, alerts, settings) {
define('admin/settings', [
'uploader', 'mousetrap', 'hooks', 'alerts', 'settings', 'bootstrap',
], function (uploader, mousetrap, hooks, alerts, settings, bootstrap) {
const Settings = {};
Settings.populateTOC = function () {
const headers = $('.settings-header');
const tocEl = $('[component="settings/toc"]');
const tocList = $('[component="settings/toc/list"]');
const mainHader = $('[component="settings/main/header"]');
if (headers.length > 1) {
if (headers.length > 1 && tocList.length) {
headers.each(function () {
const header = $(this).text();
const anchor = header.toLowerCase().replace(/ /g, '-').trim();
const anchor = header.toLowerCase()
.replace(/ /g, '-')
.replace(/&/g, '-')
.trim();
$(this).parent().attr('id', anchor);
tocList.append(`<a class="btn-ghost-sm text-xs justify-content-start text-decoration-none" href="#${anchor}">${header}</a>`);
});
const offset = mainHader.outerHeight(true);
// https://stackoverflow.com/a/11814275/583363
tocList.find('a').on('click', function (event) {
event.preventDefault();
const href = $(this).attr('href');
$(href)[0].scrollIntoView();
window.location.hash = href;
scrollBy(0, -offset);
setTimeout(() => {
tocList.find('a').removeClass('active');
$(this).addClass('active');
}, 10);
return false;
});
$(this).prepend('<a name="' + anchor + '"></a>');
$('.section-content ul').append('<li><a href="#' + anchor + '">' + header + '</a></li>');
new bootstrap.ScrollSpy($('#spy-container')[0], {
target: '#settings-navbar',
rootMargin: '-10% 0px -70%',
smoothScroll: true,
});
const scrollTo = $('a[name="' + window.location.hash.replace('#', '') + '"]');
const scrollTo = $(`${window.location.hash}`);
if (scrollTo.length) {
$('html, body').animate({
scrollTop: (scrollTo.offset().top) + 'px',
scrollTop: (scrollTo.offset().top - offset) + 'px',
}, 400);
}
} else {
$('.content-header').parents('.row').remove();
tocEl.removeClass('hidden');
}
};
@ -117,7 +143,7 @@ define('admin/settings', ['uploader', 'mousetrap', 'hooks', 'alerts', 'settings'
saveBtnEl.classList.toggle('saved', true);
setTimeout(() => {
saveBtnEl.classList.toggle('saved', false);
}, 5000);
}, 2500);
}
};

@ -20,7 +20,19 @@ define('admin/settings/general', ['admin/settings'], function () {
$('button[data-action="removeOgImage"]').on('click', function () {
$('input[data-field="removeOgImage"]').val('');
});
$('[data-field="homePageRoute"]').on('change', toggleCustomRoute);
toggleCustomRoute();
};
function toggleCustomRoute() {
if ($('[data-field="homePageRoute"]').val() === 'custom') {
$('#homePageCustom').show();
} else {
$('#homePageCustom').hide();
}
}
return Module;
});

@ -1,22 +0,0 @@
'use strict';
define('admin/settings/homepage', ['admin/settings'], function () {
function toggleCustomRoute() {
if ($('[data-field="homePageRoute"]').val() === 'custom') {
$('#homePageCustom').show();
} else {
$('#homePageCustom').hide();
}
}
const Homepage = {};
Homepage.init = function () {
$('[data-field="homePageRoute"]').on('change', toggleCustomRoute);
toggleCustomRoute();
};
return Homepage;
});

@ -163,13 +163,12 @@ define('admin/settings/navigation', [
function toggle() {
const btn = $(this);
const disabled = btn.hasClass('btn-success');
const disabled = btn.hasClass('enable');
const index = btn.parents('[data-index]').attr('data-index');
translator.translate(disabled ? '[[admin/settings/navigation:btn.disable]]' : '[[admin/settings/navigation:btn.enable]]', function (html) {
btn.toggleClass('btn-warning').toggleClass('btn-success').html(html);
btn.parents('li').find('[name="enabled"]').val(disabled ? 'on' : '');
$('#active-navigation [data-index="' + index + '"] a').toggleClass('text-muted', !disabled);
});
btn.siblings('.toggle').removeClass('hidden');
btn.addClass('hidden');
btn.parents('li').find('[name="enabled"]').val(disabled ? 'on' : '');
$('#active-navigation [data-index="' + index + '"] a').toggleClass('text-muted', !disabled);
return false;
}

@ -1,27 +0,0 @@
'use strict';
define('admin/settings/social', ['alerts'], function (alerts) {
const social = {};
social.init = function () {
$('#save').on('click', function () {
const networks = [];
$('#postSharingNetworks input[type="checkbox"]').each(function () {
if ($(this).prop('checked')) {
networks.push($(this).attr('id'));
}
});
socket.emit('admin.social.savePostSharingNetworks', networks, function (err) {
if (err) {
return alerts.error(err);
}
alerts.success('[[admin/settings/social:save-success]]');
});
});
};
return social;
});

@ -22,7 +22,8 @@ define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, b
title: '[[groups:details.add-member]]',
message: html,
buttons: {
ok: {
OK: {
label: '[[groups:details.add-member]]',
callback: function () {
const users = [];
modal.find('[data-uid][data-selected]').each(function (index, el) {

@ -9,7 +9,7 @@ define('categoryFilter', ['categorySearch', 'api', 'hooks'], function (categoryS
}
options = options || {};
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.template = 'partials/category/filter-dropdown-left';
options.template = options.template || 'partials/category/filter-dropdown-left';
hooks.fire('action:category.filter.options', { el: el, options: options });

@ -8,6 +8,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) {
options = options || {};
options.privilege = options.privilege || 'topics:read';
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.cacheList = options.hasOwnProperty('cacheList') ? options.cacheList : true;
let localCategories = [];
if (Array.isArray(options.localCategories)) {
@ -36,7 +37,7 @@ define('categorySearch', ['alerts', 'bootstrap'], function (alerts, bootstrap) {
const val = searchEl.find('input').val();
if (val.length > 1 || (!val && !categoriesList)) {
loadList(val, function (categories) {
categoriesList = categoriesList || categories;
categoriesList = options.cacheList && (categoriesList || categories);
renderList(categories);
});
} else if (!val && categoriesList) {

@ -1,8 +1,8 @@
'use strict';
define('categorySelector', [
'categorySearch', 'bootbox', 'hooks',
], function (categorySearch, bootbox, hooks) {
'categorySearch', 'bootbox', 'hooks', 'translator',
], function (categorySearch, bootbox, hooks, translator) {
const categorySelector = {};
categorySelector.init = function (el, options) {
@ -13,7 +13,7 @@ define('categorySelector', [
const onSelect = options.onSelect || function () {};
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
options.template = 'partials/category/selector-dropdown-left';
options.template = options.template || 'partials/category/selector-dropdown-left';
hooks.fire('action:category.selector.options', { el: el, options: options });
categorySearch.init(el, options);
@ -22,15 +22,21 @@ define('categorySelector', [
el: el,
selectedCategory: null,
};
el.on('click', '[data-cid]', function () {
const categoryEl = $(this);
if (categoryEl.hasClass('disabled')) {
return false;
}
selector.selectCategory(categoryEl.attr('data-cid'));
onSelect(selector.selectedCategory);
return onSelect(selector.selectedCategory);
});
let defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
translator.translate(defaultSelectHtml, (translated) => {
defaultSelectHtml = translated;
});
const defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html();
selector.selectCategory = function (cid) {
const categoryEl = selector.el.find('[data-cid="' + cid + '"]');
selector.selectedCategory = {
@ -54,6 +60,14 @@ define('categorySelector', [
selector.getSelectedCid = function () {
return selector.selectedCategory ? selector.selectedCategory.cid : 0;
};
if (options.hasOwnProperty('selectedCategory')) {
app.parseAndTranslate(options.template, { selectedCategory: options.selectedCategory }, function (html) {
selector.el.find('[component="category-selector-selected"]').html(
html.find('[component="category-selector-selected"]').html()
);
});
}
return selector;
};

@ -323,7 +323,7 @@ define('iconSelect', ['benchpress', 'bootbox'], function (Benchpress, bootbox) {
}
icons.remove();
iconData.forEach((iconData) => {
iconContainer.append($(`<i class="fa fa-xl fa-${iconData.style} fa-${iconData.id}" data-label="${iconData.label}"></i>`));
iconContainer.append($(`<i class="fa fa-xl fa-${iconData.style} fa-${iconData.id} rounded-1" data-label="${iconData.label}"></i>`));
});
icons = modalEl.find('.nbb-fa-icons i');
changeSelection();

@ -364,6 +364,13 @@ Categories.buildForSelectCategories = function (categories, fields, parentCid) {
const rootCategories = categories.filter(category => category && category.parentCid === parentCid);
rootCategories.sort((a, b) => {
if (a.order !== b.order) {
return a.order - b.order;
}
return a.cid - b.cid;
});
rootCategories.forEach(category => recursive(category, categoriesData, '', 0));
const pickFields = [

@ -60,8 +60,9 @@ categoriesController.getAll = async function (req, res) {
}
const fields = [
'cid', 'name', 'icon', 'parentCid', 'disabled', 'link', 'order',
'color', 'bgColor', 'backgroundImage', 'imageClass', 'subCategoriesPerPage',
'cid', 'name', 'icon', 'parentCid', 'disabled', 'link',
'order', 'color', 'bgColor', 'backgroundImage', 'imageClass',
'subCategoriesPerPage', 'description',
];
const categoriesData = await categories.getCategoriesFields(cids, fields);
const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields });
@ -101,6 +102,7 @@ categoriesController.getAll = async function (req, res) {
breadcrumbs: crumbs,
pagination: pagination.create(page, pageCount, req.query),
categoriesPerPage: meta.config.categoriesPerPage,
selectCategoryLabel: '[[admin/manage/categories:jump-to]]',
});
};

@ -18,13 +18,27 @@ const settingsController = module.exports;
settingsController.get = async function (req, res) {
const term = req.params.term || 'general';
res.render(`admin/settings/${term}`);
const payload = {
title: `[[admin/menu:settings/${term}]]`,
};
if (term === 'general') {
payload.routes = await helpers.getHomePageRoutes(req.uid);
payload.postSharing = await social.getPostSharing();
const languageData = await languages.list();
languageData.forEach((language) => {
language.selected = language.code === meta.config.defaultLang;
});
payload.languages = languageData;
payload.autoDetectLang = meta.config.autoDetectLang;
}
res.render(`admin/settings/${term}`, payload);
};
settingsController.email = async (req, res) => {
const emails = await emailer.getTemplates(meta.config);
res.render('admin/settings/email', {
title: '[[admin/menu:settings/email]]',
emails: emails,
sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path),
services: emailer.listServices(),
@ -38,6 +52,7 @@ settingsController.user = async (req, res) => {
label: `[[notifications:${type}]]`,
}));
res.render('admin/settings/user', {
title: '[[admin/menu:settings/user]]',
notificationSettings: notificationSettings,
});
};
@ -45,6 +60,7 @@ settingsController.user = async (req, res) => {
settingsController.post = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/post', {
title: '[[admin/menu:settings/post]]',
groupsExemptFromPostQueue: groupData,
});
};
@ -52,22 +68,11 @@ settingsController.post = async (req, res) => {
settingsController.advanced = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/advanced', {
title: '[[admin/menu:settings/advanced]]',
groupsExemptFromMaintenanceMode: groupData,
});
};
settingsController.languages = async function (req, res) {
const languageData = await languages.list();
languageData.forEach((language) => {
language.selected = language.code === meta.config.defaultLang;
});
res.render('admin/settings/languages', {
languages: languageData,
autoDetectLang: meta.config.autoDetectLang,
});
};
settingsController.navigation = async function (req, res) {
const [admin, allGroups] = await Promise.all([
navigationAdmin.getAdmin(),
@ -94,23 +99,14 @@ settingsController.navigation = async function (req, res) {
});
admin.navigation = admin.enabled.slice();
admin.title = '[[admin/menu:settings/navigation]]';
res.render('admin/settings/navigation', admin);
};
settingsController.homepage = async function (req, res) {
const routes = await helpers.getHomePageRoutes(req.uid);
res.render('admin/settings/homepage', { routes: routes });
};
settingsController.social = async function (req, res) {
const posts = await social.getPostSharing();
res.render('admin/settings/social', {
posts: posts,
});
};
settingsController.api = async (req, res) => {
const tokens = await api.utils.tokens.list();
res.render('admin/settings/api', { tokens });
res.render('admin/settings/api', {
title: '[[admin/menu:settings/api]]',
tokens,
});
};

@ -31,10 +31,8 @@ const buildImports = {
},
admin: function (source) {
return [
'@import "admin/vars";',
'@import "bootswatch/dist/materia/variables";',
'@import "admin/overrides";',
'@import "bootstrap/scss/bootstrap";',
'@import "bootswatch/dist/materia/bootswatch";',
'@import "mixins";',
'@import "fontawesome";',
'@import "@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput";',

@ -83,6 +83,10 @@ JS.buildModules = async function () {
JS.linkStatics = async function () {
await fs.promises.rm(path.join(__dirname, '../../build/public/plugins'), { recursive: true, force: true });
plugins.staticDirs['core/inter'] = path.join(basePath, 'node_modules//@fontsource/inter/files');
plugins.staticDirs['core/poppins'] = path.join(basePath, 'node_modules//@fontsource/poppins/files');
await Promise.all(Object.keys(plugins.staticDirs).map(async (mappedPath) => {
const sourceDir = plugins.staticDirs[mappedPath];
const destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);

@ -142,6 +142,14 @@ module.exports = function (Posts) {
filePaths = [filePaths];
}
if (process.platform === 'win32') {
// windows path => 'files\\1685368788211-1-profileimg.jpg'
// turn it into => 'files/1685368788211-1-profileimg.jpg'
filePaths.forEach((file) => {
file.path = file.path.split(path.sep).join(path.posix.sep);
});
}
const keys = filePaths.map(fileObj => `upload:${md5(fileObj.path.replace('-resized', ''))}:pids`);
return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1)));
};

@ -35,10 +35,7 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user);
helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post);
helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced);
helpers.setupAdminPageRoute(app, `/${name}/settings/languages`, middlewares, controllers.admin.settings.languages);
helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation);
helpers.setupAdminPageRoute(app, `/${name}/settings/homepage`, middlewares, controllers.admin.settings.homepage);
helpers.setupAdminPageRoute(app, `/${name}/settings/social`, middlewares, controllers.admin.settings.social);
helpers.setupAdminPageRoute(app, `/${name}/settings/api`, middlewares, controllers.admin.settings.api);
helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get);

@ -3,6 +3,7 @@
const _ = require('lodash');
const plugins = require('./plugins');
const db = require('./database');
const meta = require('./meta');
const social = module.exports;
@ -26,9 +27,8 @@ social.getPostSharing = async function () {
},
];
networks = await plugins.hooks.fire('filter:social.posts', networks);
const activated = await db.getSetMembers('social:posts.activated');
networks.forEach((network) => {
network.activated = activated.includes(network.id);
network.activated = parseInt(meta.config[`post-sharing-${network.id}`], 10) === 1;
});
social.postSharing = networks;
@ -41,12 +41,16 @@ social.getActivePostSharing = async function () {
};
social.setActivePostSharingNetworks = async function (networkIDs) {
// keeping for 1.0.0 upgrade script that uses this function
social.postSharing = null;
await db.delete('social:posts.activated');
if (!networkIDs.length) {
return;
}
await db.setAdd('social:posts.activated', networkIDs);
const data = {};
networkIDs.forEach((id) => {
data[`post-sharing-${id}`] = 1;
});
await db.setObject('config', data);
};
require('./promisify')(social);

@ -19,7 +19,6 @@ SocketAdmin.tags = require('./admin/tags');
SocketAdmin.rewards = require('./admin/rewards');
SocketAdmin.navigation = require('./admin/navigation');
SocketAdmin.rooms = require('./admin/rooms');
SocketAdmin.social = require('./admin/social');
SocketAdmin.themes = require('./admin/themes');
SocketAdmin.plugins = require('./admin/plugins');
SocketAdmin.widgets = require('./admin/widgets');

@ -1,9 +0,0 @@
'use strict';
const social = require('../../social');
const SocketSocial = module.exports;
SocketSocial.savePostSharingNetworks = async function (socket, data) {
await social.setActivePostSharingNetworks(data);
};

@ -0,0 +1,19 @@
'use strict';
const db = require('../../database');
module.exports = {
name: 'Migrate post sharing values to config',
timestamp: Date.UTC(2023, 4, 23),
method: async () => {
const activated = await db.getSetMembers('social:posts.activated');
if (activated.length) {
const data = {};
activated.forEach((id) => {
data[`post-sharing-${id}`] = 1;
});
await db.setObject('config', data);
await db.delete('social:posts.activated');
}
},
};

@ -1,9 +1,9 @@
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row post-cache">
<div class="row post-cache settings px-lg-4">
<div class="col-lg-12">
<div class="row">
{{{each caches}}}
<div class="col-lg-3">
<div class="col-xl-3">
<div class="card">
<div class="card-header">[[admin/advanced/cache:{@key}-cache]]</div>
<div class="card-body">
@ -52,4 +52,4 @@
</div>
</div>
<!-- IMPORT admin/partials/settings/footer.tpl -->
<!-- IMPORT admin/partials/save_button.tpl -->

@ -1,146 +1,147 @@
<div class="row database">
{{{ if mongo }}}
<div class="col-lg-6">
{{{ if mongo.serverStatusError }}}
<div class="alert alert-warning">
{mongo.serverStatusError}
</div>
{{{ end }}}
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:mongo]]</div>
<div class="card-body">
<div class="database-info">
<span>[[admin/advanced/database:mongo.version]]</span> <span class="text-end">{mongo.version}</span><br/>
<hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{mongo.uptime}</span><br/>
<span>[[admin/advanced/database:mongo.storage-engine]]</span> <span class="text-end">{mongo.storageEngine}</span><br/>
<span>[[admin/advanced/database:mongo.collections]]</span> <span class="text-end formatted-number">{mongo.collections}</span><br/>
<span>[[admin/advanced/database:mongo.objects]]</span> <span class="text-end formatted-number">{mongo.objects}</span><br/>
<span>[[admin/advanced/database:mongo.avg-object-size]]</span> <span class="text-end">[[admin/advanced/database:x-b, {mongo.avgObjSize}]]</span><br/>
<hr/>
<span>[[admin/advanced/database:mongo.data-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.dataSize}]]</span><br/>
<span>[[admin/advanced/database:mongo.storage-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.storageSize}]]</span><br/>
<span>[[admin/advanced/database:mongo.index-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.indexSize}]]</span><br/>
{{{ if mongo.fileSize }}}
<span>[[admin/advanced/database:mongo.file-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.fileSize}]]</span><br/>
{{{ end }}}
<hr/>
<span>[[admin/advanced/database:mongo.resident-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.resident}]]</span><br/>
<span>[[admin/advanced/database:mongo.virtual-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.virtual}]]</span><br/>
<span>[[admin/advanced/database:mongo.mapped-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.mapped}]]</span><br/>
<hr/>
<span>[[admin/advanced/database:mongo.bytes-in]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesIn}]]</span><br/>
<span>[[admin/advanced/database:mongo.bytes-out]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesOut}]]</span><br/>
<span>[[admin/advanced/database:mongo.num-requests]]</span> <span class="text-end">{mongo.network.numRequests}</span><br/>
<div class="acp-page-container">
<div class="row database">
{{{ if mongo }}}
<div class="{{{ if redis }}}col-lg-6{{{ else }}}col-lg-12{{{ end }}}">
{{{ if mongo.serverStatusError }}}
<div class="alert alert-warning">
{mongo.serverStatusError}
</div>
{{{ end }}}
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:mongo]]</div>
<div class="card-body">
<div class="database-info">
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.version]]</span> <span class="text-end">{mongo.version}</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{mongo.uptime}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.storage-engine]]</span> <span class="text-end">{mongo.storageEngine}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.collections]]</span> <span class="text-end formatted-number">{mongo.collections}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.objects]]</span> <span class="text-end formatted-number">{mongo.objects}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.avg-object-size]]</span> <span class="text-end">[[admin/advanced/database:x-b, {mongo.avgObjSize}]]</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.data-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.dataSize}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.storage-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.storageSize}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.index-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.indexSize}]]</span></div>
{{{ if mongo.fileSize }}}
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.file-size]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.fileSize}]]</span></div>
{{{ end }}}
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.resident-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.resident}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.virtual-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.virtual}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.mapped-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.mem.mapped}]]</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.bytes-in]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesIn}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.bytes-out]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {mongo.network.bytesOut}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:mongo.num-requests]]</span> <span class="text-end">{mongo.network.numRequests}</span></div>
</div>
</div>
</div>
</div>
</div>
{{{ end }}}
{{{ end }}}
{{{ if redis }}}
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:redis]]</div>
<div class="card-body">
<div class="database-info">
<span>[[admin/advanced/database:redis.version]]</span> <span class="text-end">{redis.redis_version}</span><br/>
<hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{redis.uptime_in_seconds}</span><br/>
<span>[[admin/advanced/database:uptime-days]]</span> <span class="text-end">{redis.uptime_in_days}</span><br/>
<hr/>
<span>[[admin/advanced/database:redis.keys]]</span> <span class="text-end formatted-number">{redis.keys}</span><br/>
<span>[[admin/advanced/database:redis.expires]]</span> <span class="text-end formatted-number">{redis.expires}</span><br/>
<span>[[admin/advanced/database:redis.avg-ttl]]</span> <span class="text-end formatted-number">{redis.avg_ttl}</span><br/>
<span>[[admin/advanced/database:redis.connected-clients]]</span> <span class="text-end">{redis.connected_clients}</span><br/>
<span>[[admin/advanced/database:redis.connected-slaves]]</span> <span class="text-end">{redis.connected_slaves}</span><br/>
<span>[[admin/advanced/database:redis.blocked-clients]]</span> <span class="text-end">{redis.blocked_clients}</span><br/>
<hr/>
{{{ if redis }}}
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:redis]]</div>
<div class="card-body">
<div class="database-info">
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.version]]</span> <span class="text-end">{redis.redis_version}</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{redis.uptime_in_seconds}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-days]]</span> <span class="text-end">{redis.uptime_in_days}</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keys]]</span> <span class="text-end formatted-number">{redis.keys}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.expires]]</span> <span class="text-end formatted-number">{redis.expires}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.avg-ttl]]</span> <span class="text-end formatted-number">{redis.avg_ttl}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.connected-clients]]</span> <span class="text-end">{redis.connected_clients}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.connected-slaves]]</span> <span class="text-end">{redis.connected_slaves}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.blocked-clients]]</span> <span class="text-end">{redis.blocked_clients}</span></div>
<hr/>
<span>[[admin/advanced/database:redis.used-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.used_memory_human}]]</span><br/>
<span>[[admin/advanced/database:redis.memory-frag-ratio]]</span> <span class="text-end">{redis.mem_fragmentation_ratio}</span><br/>
<hr/>
<span>[[admin/advanced/database:redis.total-connections-recieved]]</span> <span class="text-end formatted-number">{redis.total_connections_received}</span><br/>
<span>[[admin/advanced/database:redis.total-commands-processed]]</span> <span class="text-end formatted-number">{redis.total_commands_processed}</span><br/>
<span>[[admin/advanced/database:redis.iops]]</span> <span class="text-end formatted-number">{redis.instantaneous_ops_per_sec}</span><br/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.used-memory]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.used_memory_human}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.memory-frag-ratio]]</span> <span class="text-end">{redis.mem_fragmentation_ratio}</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-connections-recieved]]</span> <span class="text-end formatted-number">{redis.total_connections_received}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-commands-processed]]</span> <span class="text-end formatted-number">{redis.total_commands_processed}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.iops]]</span> <span class="text-end formatted-number">{redis.instantaneous_ops_per_sec}</span></div>
<span>[[admin/advanced/database:redis.iinput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_input}]]</span><br/>
<span>[[admin/advanced/database:redis.ioutput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_output}]]</span><br/>
<span>[[admin/advanced/database:redis.total-input]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_input}]]</span><br/>
<span>[[admin/advanced/database:redis.total-output]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_output}]]</span><br/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.iinput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_input}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.ioutput]]</span> <span class="text-end">[[admin/advanced/database:x-mb, {redis.instantaneous_output}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-input]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_input}]]</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.total-output]]</span> <span class="text-end">[[admin/advanced/database:x-gb, {redis.total_net_output}]]</span></div>
<hr/>
<span>[[admin/advanced/database:redis.keyspace-hits]]</span> <span class="text-end formatted-number">{redis.keyspace_hits}</span><br/>
<span>[[admin/advanced/database:redis.keyspace-misses]]</span> <span class="text-end formatted-number">{redis.keyspace_misses}</span><br/>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keyspace-hits]]</span> <span class="text-end formatted-number">{redis.keyspace_hits}</span></div>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:redis.keyspace-misses]]</span> <span class="text-end formatted-number">{redis.keyspace_misses}</span></div>
</div>
</div>
</div>
</div>
</div>
{{{ end }}}
{{{ end }}}
{{{ if postgres }}}
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:postgres]]</div>
<div class="card-body">
<div class="database-info">
<span>[[admin/advanced/database:postgres.version]]</span> <span class="text-end">{postgres.version}</span><br/>
<hr/>
<span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{postgres.uptime}</span><br/>
{{{ if postgres }}}
<div class="col-lg-6">
<div class="card">
<div class="card-header"><i class="fa fa-hdd-o"></i> [[admin/advanced/database:postgres]]</div>
<div class="card-body">
<div class="database-info">
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:postgres.version]]</span> <span class="text-end">{postgres.version}</span></div>
<hr/>
<div class="d-flex justify-content-between"><span>[[admin/advanced/database:uptime-seconds]]</span> <span class="text-end formatted-number">{postgres.uptime}</span></div>
</div>
</div>
</div>
</div>
{{{ end }}}
</div>
{{{ end }}}
</div>
<div class="row database">
{{{ if mongo }}}
<div class="col-lg-6">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".mongodb-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:mongo.raw-info]]
</h5>
<div class="row">
{{{ if mongo }}}
<div class="{{{ if redis }}}col-lg-6{{{ else }}}col-lg-12{{{ end }}}">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".mongodb-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:mongo.raw-info]]
</h5>
<div class="card-body mongodb-raw collapse">
<div class="highlight">
<pre>{mongo.raw}</pre>
<div class="card-body mongodb-raw collapse">
<div class="highlight">
<pre>{mongo.raw}</pre>
</div>
</div>
</div>
</div>
</div>
{{{ end }}}
{{{ end }}}
{{{ if redis }}}
<div class="col-lg-6">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".redis-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:redis.raw-info]]
</h5>
{{{ if redis }}}
<div class="col-lg-6">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".redis-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:redis.raw-info]]
</h5>
<div class="card-body redis-raw collapse">
<div class="highlight">
<pre>{redis.raw}</pre>
<div class="card-body redis-raw collapse">
<div class="highlight">
<pre>{redis.raw}</pre>
</div>
</div>
</div>
</div>
</div>
{{{ end }}}
{{{ end }}}
{{{ if postgres }}}
<div class="col-lg-6">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".postgresql-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:postgres.raw-info]]
</h5>
{{{ if postgres }}}
<div class="col-lg-6">
<div class="card">
<h5 class="card-header" data-bs-toggle="collapse" data-bs-target=".postgresql-raw">
<i class="fa fa-caret-down"></i> [[admin/advanced/database:postgres.raw-info]]
</h5>
<div class="card-body postgresql-raw collapse">
<div class="highlight">
<pre>{postgres.raw}</pre>
<div class="card-body postgresql-raw collapse">
<div class="highlight">
<pre>{postgres.raw}</pre>
</div>
</div>
</div>
</div>
{{{ end }}}
</div>
{{{ end }}}
</div>
</div>

@ -1,77 +1,79 @@
<div class="row">
<div class="col-lg-9">
<div class="row">
<div class="col-sm-6 text-center">
<div class="card">
<div class="card-body">
<div><canvas id="not-found" height="250"></canvas></div>
<div class="px-lg-4">
<div class="row">
<div class="col-lg-9">
<div class="row">
<div class="col-sm-6 text-center">
<div class="card">
<div class="card-body">
<div><canvas id="not-found" height="250"></canvas></div>
</div>
<div class="card-footer"><small>
<strong>[[admin/advanced/errors:figure-x, 1]]</strong> &ndash;
[[admin/advanced/errors:error-events-per-day, [[admin/advanced/errors:error.404]]]]
</small></div>
</div>
<div class="card-footer"><small>
<strong>[[admin/advanced/errors:figure-x, 1]]</strong> &ndash;
[[admin/advanced/errors:error-events-per-day, [[admin/advanced/errors:error.404]]]]
</small></div>
</div>
</div>
<div class="col-sm-6 text-center">
<div class="card">
<div class="card-body">
<div><canvas id="toobusy" height="250"></canvas></div>
<div class="col-sm-6 text-center">
<div class="card">
<div class="card-body">
<div><canvas id="toobusy" height="250"></canvas></div>
</div>
<div class="card-footer"><small>
<strong>[[admin/advanced/errors:figure-x, 2]]</strong> &ndash;
[[admin/advanced/errors:error-events-per-day, [[admin/advanced/errors:error.503]]]]
</small></div>
</div>
<div class="card-footer"><small>
<strong>[[admin/advanced/errors:figure-x, 2]]</strong> &ndash;
[[admin/advanced/errors:error-events-per-day, [[admin/advanced/errors:error.503]]]]
</small></div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 acp-sidebar">
<div class="card">
<div class="card-header">[[admin/advanced/errors:manage-error-log]]</div>
<div class="card-body">
<div class="d-grid gap-2" role="group">
<a class="btn btn-info" target="_top" href="{config.relative_path}/admin/advanced/errors/export">
<i class="fa fa-download"></i> [[admin/advanced/errors:export-error-log]]
</a>
<button class="btn btn-danger" data-action="clear">
<i class="fa fa-trash"></i> [[admin/advanced/errors:clear-error-log]]
</button>
<div class="col-lg-3 acp-sidebar">
<div class="card">
<div class="card-header">[[admin/advanced/errors:manage-error-log]]</div>
<div class="card-body">
<div class="d-grid gap-2" role="group">
<a class="btn btn-info" target="_top" href="{config.relative_path}/admin/advanced/errors/export">
<i class="fa fa-download"></i> [[admin/advanced/errors:export-error-log]]
</a>
<button class="btn btn-danger" data-action="clear">
<i class="fa fa-trash"></i> [[admin/advanced/errors:clear-error-log]]
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="fa fa-exclamation-triangle"></i> [[admin/advanced/errors:error.404]]
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<th>[[admin/advanced/errors:route]]</th>
<th>[[admin/advanced/errors:count]]</th>
</thead>
<tbody>
{{{ each not-found }}}
<tr>
<td>{./value}</td>
<td>{./score}</td>
</tr>
{{{ end }}}
{{{ if !not-found.length }}}
<tr>
<td colspan="2">
<div class="alert alert-success">
[[admin/advanced/errors:no-routes-not-found]]
</div>
</td>
</tr>
{{{ end }}}
</tbody>
</table>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<i class="fa fa-exclamation-triangle"></i> [[admin/advanced/errors:error.404]]
</div>
<div class="card-body">
<table class="table">
<thead>
<th>[[admin/advanced/errors:route]]</th>
<th>[[admin/advanced/errors:count]]</th>
</thead>
<tbody>
{{{ each not-found }}}
<tr>
<td>{./value}</td>
<td>{./score}</td>
</tr>
{{{ end }}}
{{{ if !not-found.length }}}
<tr>
<td colspan="2">
<div class="alert alert-success">
[[admin/advanced/errors:no-routes-not-found]]
</div>
</td>
</tr>
{{{ end }}}
</tbody>
</table>
</div>
</div>
</div>
</div>

@ -1,4 +1,4 @@
<div class="row events">
<div class="row events px-lg-4">
<div class="col-lg-9">
<h5><i class="fa fa-calendar-o"></i> [[admin/advanced/events:events]]</h5>
{{{ if !events.length }}}
@ -6,25 +6,27 @@
{{{ end }}}
<div class="events-list">
{{{ each events }}}
<div class="card mb-3">
<div class="card mb-3" data-eid="{events.eid}">
<div class="card-body">
<div data-eid="{events.eid}">
<div class="mb-3">
<div class="mb-3 d-flex flex-wrap justify-content-between align-items-center gap-1">
<div>
<span class="badge bg-primary">#{events.eid}</span>
<span class="badge bg-info">{events.type}</span>
<span class="badge bg-info">uid {events.uid}</span>
{{{ if events.ip }}}<span class="badge bg-info">{events.ip}</span>{{{ end }}}
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{buildAvatar(events.user, "24px", true)}</a>
<a href="{config.relative_path}/user/{events.user.userslug}" target="_blank">{events.user.username}</a>
<span class="float-end delete-event ms-2 pointer"><i class="fa fa-trash-o"></i></span>
<span class="float-end">{events.timestampISO}</span>
<span class="text-xs">{events.timestampISO}</span>
</div>
<div>
<button class="btn btn-light btn-sm delete-event ms-2 pointer"><i class="fa fa-trash-o text-danger"></i></button>
</div>
<pre class="text-bg-light p-3">{events.jsonString}</pre>
</div>
<pre class="text-bg-light p-3">{events.jsonString}</pre>
</div>
</div>
{{{ end }}}
<!-- IMPORT partials/paginator.tpl -->
<!-- IMPORT admin/partials/paginator.tpl -->
</div>
</div>
<div class="col-lg-3 acp-sidebar">

@ -1,9 +1,9 @@
<div class="hooks-list panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="hooks-list panel-group px-lg-4" id="accordion" role="tablist" aria-multiselectable="true">
{{{ each hooks }}}
<div class="card">
<div class="card-header" role="tab">
<h6 class="mb-0">
<a style="text-transform: none;" role="button" data-bs-toggle="collapse" data-bs-parent="#accordion" data-bs-target="#{hooks.index}" aria-expanded="true" aria-controls="{hooks.index}">{hooks.hookName}</a>
<a style="text-transform: none;" class="text-reset text-decoration-none" role="button" data-bs-toggle="collapse" data-bs-parent="#accordion" data-bs-target="#{hooks.index}" aria-expanded="true" aria-controls="{hooks.index}">{hooks.hookName}</a>
<span class="float-end">{hooks.count} hooks</span>
</h6>
</div>

@ -1,4 +1,4 @@
<div class="row logs">
<div class="row logs px-lg-4">
<div class="col-lg-9">
<div class="card">
<div class="card-header"><i class="fa fa-file-text-o"></i> [[admin/advanced/logs:logs]]</div>

@ -1,4 +1,4 @@
<div id="customise" class="customise">
<div id="customise" class="customise px-lg-4">
<ul class="nav nav-pills mb-3">
<li class="nav-item"><a class="nav-link active" href="#custom-css" data-bs-toggle="tab">[[admin/appearance/customise:custom-css]]</a></li>
<li class="nav-item"><a class="nav-link" href="#custom-js" data-bs-toggle="tab">[[admin/appearance/customise:custom-js]]</a></li>

@ -1,4 +1,4 @@
<div id="skins" class="skins">
<div id="skins" class="skins px-lg-4">
<div class="directory row text-center" id="bootstrap_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/skins:loading]]
</div>

@ -1,4 +1,4 @@
<div id="themes" class="themes">
<div id="themes" class="themes px-lg-4">
<div class="directory row text-center" id="installed_themes">
<i class="fa fa-refresh fa-spin"></i> [[admin/appearance/themes:checking-for-installed]]
</div>

@ -1,4 +1,4 @@
<div class="row dashboard">
<div class="row dashboard px-lg-4">
<div class="col-lg-9">
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
@ -70,11 +70,11 @@
<div class="card-header">[[admin/dashboard:control-panel]]</div>
<div class="card-body text-center">
<div class="d-grid gap-2 mb-2">
<button class="btn btn-block btn-warning restart"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:restart]]</button>
<button class="btn btn-block btn-danger rebuild-and-restart"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:rebuild-and-restart]]</button>
<button component="restart" class="btn btn-block btn-warning btn-sm"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:restart]]</button>
<button component="rebuild-and-restart" class="btn btn-block btn-danger btn-sm"{{{ if !canRestart }}} disabled{{{ end }}}>[[admin/dashboard:rebuild-and-restart]]</button>
</div>
{{{ if lastrestart }}}
<p>
<p class="text-sm">
[[admin/dashboard:last-restarted-by]]<br />
<a href="{config.relative_path}/uid/{lastrestart.uid}"><span class="badge bg-info">{lastrestart.user.username}</span></a> <span class="timeago" title="{lastrestart.timestampISO}"></span>
</p>
@ -87,7 +87,7 @@
{{{ end }}}
</p>
<p>
<a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block" data-bs-placement="bottom" data-bs-toggle="tooltip" title="[[admin/dashboard:maintenance-mode-title]]">[[admin/dashboard:maintenance-mode]]</a>
<a href="{config.relative_path}/admin/settings/advanced" class="btn btn-info btn-block btn-sm" data-bs-placement="bottom" data-bs-toggle="tooltip" title="[[admin/dashboard:maintenance-mode-title]]">[[admin/dashboard:maintenance-mode]]</a>
</p>
<hr />
@ -106,9 +106,9 @@
<div class="card mb-3">
<div class="card-header">[[admin/dashboard:updates]]</div>
<div class="card-body">
<div class="alert {{{ if lookupFailed }}}alert-danger{{{ else }}}{{{ if upgradeAvailable }}}alert-warning{{{ else }}}{{{ if currentPrerelease }}}alert-info{{{ else }}}alert-success{{{ end }}}{{{ end }}}{{{ end }}} version-check">
<p>[[admin/dashboard:running-version, {version}]]</p>
<p>
<div class="text-sm alert {{{ if lookupFailed }}}alert-danger{{{ else }}}{{{ if upgradeAvailable }}}alert-warning{{{ else }}}{{{ if currentPrerelease }}}alert-info{{{ else }}}alert-success{{{ end }}}{{{ end }}}{{{ end }}} version-check">
<p class="">[[admin/dashboard:running-version, {version}]]</p>
<p class="mb-0">
{{{ if lookupFailed }}}
[[admin/dashboard:latest-lookup-failed]]
{{{ else }}}
@ -128,7 +128,7 @@
{{{ end }}}
</p>
</div>
<p>
<p class="text-sm">
[[admin/dashboard:keep-updated]]
</p>
</div>

@ -1,15 +1,10 @@
<div class="row dashboard">
<div class="row dashboard px-lg-4">
<div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
<div class="alert alert-info">[[admin/dashboard:details.logins-static, {loginDays}]]</div>
<table class="table table-striped">
<table class="table">
<thead>
<th class="text-muted">[[admin/manage/users:users.username]]</th>
<th data-sort="joindate">[[admin/dashboard:details.logins-login-time]]</th>

@ -1,26 +1,22 @@
<div class="row dashboard">
<div class="row dashboard px-lg-4">
<div class="col-12">
<div class="d-flex gap-2 mb-3">
<a class="btn btn-primary me-auto align-items-center" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<div class="d-flex justify-content-end gap-2 mb-3">
<form class="row row-cols-lg-auto g-3 align-items-center" method="GET">
<div class="col-12 d-flex align-items-center gap-2">
<label class="form-label mb-0" for="start">[[admin/dashboard:start]]</label>
<input type="date" class="form-control" id="start" name="start" value="{startDate}">
<input type="date" class="form-control form-control-sm" id="start" name="start" value="{startDate}">
</div>
<div class="col-12 d-flex align-items-center gap-2">
<label class="form-label mb-0" for="end">[[admin/dashboard:end]]</label>
<input type="date" class="form-control" id="end" name="end" value="{endDate}">
<input type="date" class="form-control form-control-sm" id="end" name="end" value="{endDate}">
</div>
<div class="col-12">
<button onclick="$('form').submit();return false;"class="btn btn-primary" type="submit">Filter</button>
<button onclick="$('form').submit();return false;"class="btn btn-primary btn-sm" type="submit">Filter</button>
</div>
</form>
</div>
<table class="table table-sm table-striped search-list">
<table class="table table-sm search-list">
<thead>
<th class="text-end">Count</th>
<th>Term</th>

@ -1,14 +1,9 @@
<div class="row dashboard">
<div class="row dashboard px-lg-4">
<div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
<table class="table table-striped topics-list">
<table class="table topics-list">
<tbody>
{{{ if !topics.length}}}
<tr>

@ -1,14 +1,9 @@
<div class="dashboard">
<div class="dashboard px-lg-4">
<div class="col-12">
<a class="btn btn-primary mb-3" href="{config.relative_path}/admin/dashboard">
<i class="fa fa-chevron-left"></i>
[[admin/dashboard:back-to-dashboard]]
</a>
<!-- IMPORT admin/partials/dashboard/graph.tpl -->
<!-- IMPORT admin/partials/dashboard/stats.tpl -->
<table class="table table-striped users-list">
<table class="table users-list">
<thead>
<th class="text-muted">[[admin/manage/users:users.uid]]</th>
<th class="text-muted">[[admin/manage/users:users.username]]</th>

@ -7,23 +7,23 @@
<div class="card-body">
<span>[[admin/development/info:nodes-responded, {nodeCount}, {timeout}]]</span>
<table class="table table-striped">
<table class="table table-sm text-sm">
<thead>
<tr>
<td>[[admin/development/info:host]]</td>
<td class="text-center">[[admin/development/info:primary]]</td>
<td>[[admin/development/info:pid]]</td>
<td>[[admin/development/info:nodejs]]</td>
<td>[[admin/development/info:online]]</td>
<td>[[admin/development/info:git]]</td>
<td>[[admin/development/info:cpu-usage]]</td>
<td>[[admin/development/info:process-memory]]</td>
<td>[[admin/development/info:system-memory]]</td>
<td>[[admin/development/info:load]]</td>
<td>[[admin/development/info:uptime]]</td>
<td class="fw-bold">[[admin/development/info:host]]</td>
<td class="fw-bold text-center">[[admin/development/info:primary]]</td>
<td class="fw-bold">[[admin/development/info:pid]]</td>
<td class="fw-bold">[[admin/development/info:nodejs]]</td>
<td class="fw-bold">[[admin/development/info:online]]</td>
<td class="fw-bold">[[admin/development/info:git]]</td>
<td class="fw-bold">[[admin/development/info:cpu-usage]]</td>
<td class="fw-bold">[[admin/development/info:process-memory]]</td>
<td class="fw-bold">[[admin/development/info:system-memory]]</td>
<td class="fw-bold">[[admin/development/info:load]]</td>
<td class="fw-bold">[[admin/development/info:uptime]]</td>
</tr>
</thead>
<tbody>
<tbody class="text-xs">
{{{ each info }}}
<tr>
<td>{info.os.hostname}:{info.process.port}</td>

@ -1,5 +1,4 @@
<!-- IMPORT admin/partials/settings/header.tpl -->
<div class="row logger">
<div class="row logger settings px-lg-4">
<div class="col-lg-12">
<div class="card">
<div class="card-header">[[admin/development/logger:logger-settings]]</div>
@ -34,4 +33,4 @@
</div>
</div>
<!-- IMPORT admin/partials/settings/footer.tpl -->
<!-- IMPORT admin/partials/save_button.tpl -->

@ -1,141 +1,137 @@
{{{ if !canChangeState }}}
<div class="alert alert-warning">[[error:plugins-set-in-configuration]]</div>
{{{ end }}}
<ul class="nav nav-pills mb-3">
<li class="nav-item">
<button class="nav-link" data-bs-target="#trending" data-bs-toggle="tab">
[[admin/extend/plugins:trending]]
<i class="fa fa-star"></i>
</button>
</li>
<li class="nav-item">
<button class="nav-link active" data-bs-target="#installed" data-bs-toggle="tab">
[[admin/extend/plugins:installed]]
<span class="badge bg-light">{installedCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#active" data-bs-toggle="tab">
[[admin/extend/plugins:active]]
<span class="badge bg-light">{activeCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#deactive" data-bs-toggle="tab">
[[admin/extend/plugins:inactive]]
<span class="badge bg-light">{inactiveCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#upgrade" data-bs-toggle="tab">
[[admin/extend/plugins:out-of-date]]
<span class="badge bg-light">{upgradeCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#download" data-bs-toggle="tab">[[admin/extend/plugins:find-plugins]]</button>
</li>
</ul>
<div class="plugins row">
<div class="col-lg-9">
<div class="tab-content">
<div class="tab-pane fade" id="trending">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="trending">
{{{ each trending }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
<div class="tab-pane fade show active" id="installed">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="installed">
{{{ each installed }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
<div class="tab-pane fade" id="active">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="active"></ul>
</div>
<div class="tab-pane fade" id="deactive">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="deactive"></ul>
</div>
<div class="tab-pane fade" id="upgrade">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="upgrade"></ul>
</div>
<div class="tab-pane fade" id="download">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="download">
{{{ each download }}}
<!-- IMPORT admin/partials/download_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
<div class="tags d-flex flex-column gap-2 px-lg-4">
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class="">
<h4 class="fw-bold tracking-tight mb-0">[[admin/extend/plugins:plugins]]</h4>
</div>
</div>
<div class="acp-sidebar col-lg-3">
<div class="card">
<div class="card-header">[[admin/extend/plugins:plugin-search]]</div>
<div class="card-body">
<input autofocus class="form-control" type="text" id="plugin-search" placeholder="[[admin/extend/plugins:plugin-search-placeholder]]"/><br/>
</div>
<div class="d-flex align-items-center gap-1">
<input autofocus class="form-control form-control-sm" type="text" id="plugin-search" placeholder="[[admin/extend/plugins:plugin-search-placeholder]]"/><br/>
<button class="btn btn-primary btn-sm text-nowrap" id="plugin-order">[[admin/extend/plugins:order-active]]</button>
</div>
</div>
<div class="">
{{{ if !canChangeState }}}
<div class="alert alert-warning">[[error:plugins-set-in-configuration]]</div>
{{{ end }}}
<ul class="nav nav-pills mb-3">
<li class="nav-item">
<button class="nav-link" data-bs-target="#trending" data-bs-toggle="tab">
[[admin/extend/plugins:trending]]
<i class="fa fa-star"></i>
</button>
</li>
<li class="nav-item">
<button class="nav-link active" data-bs-target="#installed" data-bs-toggle="tab">
[[admin/extend/plugins:installed]]
<span class="badge text-bg-light">{installedCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#active" data-bs-toggle="tab">
[[admin/extend/plugins:active]]
<span class="badge text-bg-light">{activeCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#deactive" data-bs-toggle="tab">
[[admin/extend/plugins:inactive]]
<span class="badge text-bg-light">{inactiveCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#upgrade" data-bs-toggle="tab">
[[admin/extend/plugins:out-of-date]]
<span class="badge text-bg-light">{upgradeCount}</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link" data-bs-target="#download" data-bs-toggle="tab">[[admin/extend/plugins:find-plugins]]</button>
</li>
</ul>
<div class="card">
<div class="card-body">
<div class="form-check">
<input id="plugin-submit-usage" class="form-check-input" type="checkbox" data-field="submitPluginUsage" {{{ if submitPluginUsage }}}checked{{{ end }}}/>
<label for="plugin-submit-usage" class="form-check-label">[[admin/extend/plugins:submit-anonymous-usage]]</label>
<div class="plugins row px-2">
<div class="col-lg-9">
<div class="tab-content">
<div class="tab-pane fade" id="trending">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="trending list-unstyled">
{{{ each trending }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
<div class="tab-pane fade show active" id="installed">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="installed list-unstyled">
{{{ each installed }}}
<!-- IMPORT admin/partials/installed_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
<div class="tab-pane fade" id="active">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="active list-unstyled"></ul>
</div>
<div class="tab-pane fade" id="deactive">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="deactive list-unstyled"></ul>
</div>
<div class="tab-pane fade" id="upgrade">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="upgrade list-unstyled"></ul>
</div>
<div class="tab-pane fade" id="download">
<!-- IMPORT admin/partials/plugins/no-plugins.tpl -->
<ul class="download list-unstyled">
{{{ each download }}}
<!-- IMPORT admin/partials/download_plugin_item.tpl -->
{{{ end }}}
</ul>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">[[admin/extend/plugins:reorder-plugins]]</div>
<div class="card-body d-grid">
<button class="btn btn-outline-secondary" id="plugin-order">[[admin/extend/plugins:order-active]]</button>
</div>
</div>
<div class="card">
<div class="card-header">[[admin/extend/plugins:dev-interested]]</div>
<div class="card-body">
<p>
[[admin/extend/plugins:docs-info]]
</p>
<div class="acp-sidebar col-lg-3">
<div class="card">
<div class="card-body">
<div class="form-check form-switch text-sm">
<input id="plugin-submit-usage" class="form-check-input" type="checkbox" data-field="submitPluginUsage" {{{ if submitPluginUsage }}}checked{{{ end }}}/>
<label for="plugin-submit-usage" class="form-check-label">[[admin/extend/plugins:submit-anonymous-usage]]</label>
</div>
<hr/>
<div>
<div class="fw-semibold text-sm">[[admin/extend/plugins:dev-interested]]</div>
<p class="text-xs text-muted">
[[admin/extend/plugins:docs-info]]
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="order-active-plugins-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">[[admin/extend/plugins:order-active]]</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
</div>
<div class="modal-body">
<p>
[[admin/extend/plugins:order.description]]
</p>
<p>
[[admin/extend/plugins:order.explanation]]
</p>
<ul class="plugin-list"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">[[global:buttons.close]]</button>
<button type="button" class="btn btn-primary" id="save-plugin-order">[[global:save]]</button>
<div class="modal fade" id="order-active-plugins-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">[[admin/extend/plugins:order-active]]</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>
</div>
<div class="modal-body">
<p>
[[admin/extend/plugins:order.description]]
</p>
<p>
[[admin/extend/plugins:order.explanation]]
</p>
<ul class="plugin-list"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">[[global:buttons.close]]</button>
<button type="button" class="btn btn-primary" id="save-plugin-order">[[global:save]]</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

@ -1,80 +1,81 @@
<div id="rewards">
<ul id="active">
{{{ each active }}}
<li data-rid="{active.rid}" data-id="{active.id}">
<div class="row">
<div class="col-12 col-lg-8">
<form class="main d-inline-block">
<div class="card card-body d-inline-block if-block">
<div class="tags d-flex flex-column gap-2 px-lg-4">
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class="">
<h4 class="fw-bold tracking-tight mb-0">[[admin/extend/rewards:rewards]]</h4>
</div>
<div class="d-flex align-items-center gap-1">
<button id="new" class="btn btn-light btn-sm text-nowrap" type="button">
<i class="fa fa-fw fa-plus"></i> [[admin/extend/rewards:add-reward]]
</button>
<button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary w-100 text-center text-nowrap">[[admin/admin:save-changes]]</button>
</div>
</div>
<div id="rewards" class="">
<ul id="active" class="list-unstyled p-0 m-0">
{{{ each active }}}
<li data-rid="{active.rid}" data-id="{active.id}">
<div class="d-flex gap-1 mb-3 flex-wrap">
<form class="main d-flex gap-1">
<div class="card card-body m-0 if-block border-info border border-2">
<label class="form-label" for="condition-if-users">[[admin/extend/rewards:condition-if-users]]</label>
<select id="condition-if-users" class="form-select" name="condition" data-selected="{active.condition}">
<select id="condition-if-users" class="form-select form-select-sm" name="condition" data-selected="{active.condition}">
{{{ each conditions }}}
<option value="{conditions.condition}">{conditions.name}</option>
{{{ end }}}
</select>
</div>
<div class="card card-body d-inline-block this-block">
<div class="card card-body m-0 this-block border-warning border border-2">
<label class="form-label" for="condition-is">[[admin/extend/rewards:condition-is]]</label>
<div class="row">
<div class="col-4">
<select id="condition-is" class="form-select" name="conditional" data-selected="{active.conditional}">
{{{ each conditionals }}}
<option value="{conditionals.conditional}">{conditionals.name}</option>
{{{ end }}}
</select>
</div>
<div class="col-8">
<input class="form-control" type="text" name="value" value="{active.value}" />
</div>
<div class="d-flex gap-1 flex-nowrap">
<select id="condition-is" class="form-select form-select-sm" name="conditional" data-selected="{active.conditional}" style="max-width: 64px;">
{{{ each conditionals }}}
<option value="{conditionals.conditional}">{conditionals.name}</option>
{{{ end }}}
</select>
<input class="form-control form-control-sm" type="text" name="value" value="{active.value}" style="max-width: 64px;"/>
</div>
</div>
<div class="card card-body d-inline-block then-block">
<div class="card card-body m-0 then-block border-primary border border-2">
<label class="form-label" for="condition-then">[[admin/extend/rewards:condition-then]]</label>
<select id="condition-then" class="form-select" name="rid" data-selected="{active.rid}">
<select id="condition-then" class="form-select form-select-sm" name="rid" data-selected="{active.rid}">
{{{ each ../../rewards }}}
<option value="{rewards.rid}">{rewards.name}</option>
{{{ end }}}
</select>
</div>
</form>
</div>
<div class="col-12 col-lg-4">
<form class="rewards d-inline-block">
<div class="inputs card card-body d-inline-block reward-block"></div>
<form class="rewards">
<div class="inputs card card-body m-0 h-100 reward-block border-success border border-2"><div class="d-flex h-100 align-items-center">[[admin/extend/rewards:select-reward]]</div></div>
</form>
</div>
</div>
<div class="float-start">
<div class="card-body d-inline-block">
<form class="main">
<label class="form-label" for="claimable">[[admin/extend/rewards:max-claims]] <small>[[admin/extend/rewards:zero-infinite]]</small></label>
<input id="claimable" class="form-control" type="text" name="claimable" value="{active.claimable}" placeholder="1" />
<div class="d-flex justify-content-between align-items-center">
<form class="main d-flex gap-1 align-items-start gap-2">
<div class="d-flex flex-column gap-0">
<label class="form-label" for="claimable">[[admin/extend/rewards:max-claims]]</label>
<p class="form-text mb-0">
[[admin/extend/rewards:zero-infinite]]
</p>
</div>
<input id="claimable" class="form-control form-control-sm" type="text" name="claimable" value="{active.claimable}" placeholder="1" style="max-width: 64px;"/>
</form>
</div>
</div>
<div class="">
<div class="float-end">
<div class="card-body d-inline-block">
<button class="btn btn-danger delete">[[admin/extend/rewards:delete]]</button>
{{{ if active.disabled }}}
<button class="btn btn-success toggle">[[admin/extend/rewards:enable]]</button>
{{{ else }}}
<button class="btn btn-warning toggle">[[admin/extend/rewards:disable]]</button>
{{{ end }}}
</div>
</div>
<div class="clearfix"></div>
</li>
{{{ end }}}
</ul>
</div>
<button class="btn btn-light btn-sm toggle disable {{{ if active.disabled }}}hidden{{{ end }}}"><i class="fa fa-ban text-danger"></i> [[admin/extend/rewards:disable]]</button>
<div class="floating-button">
<button id="new" class="btn btn-primary position-fixed end-0 px-3 py-2 mb-4 me-4 rounded-circle fs-4" type="button" style="width: 64px; height: 64px;">
<i class="fa fa-fw fa-plus"></i>
</button>
<button class="btn btn-light btn-sm toggle enable {{{ if !active.disabled }}}hidden{{{ end }}}"><i class="fa fa-check text-success"></i> [[admin/extend/rewards:enable]]</button>
<button class="btn btn-light btn-sm delete"><i class="fa fa-trash text-danger"></i> [[admin/extend/rewards:delete]]</button>
</div>
</div>
<!-- IMPORT admin/partials/save_button.tpl -->
<hr/>
</li>
{{{ end }}}
</ul>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save