Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pleroma/admin-fe
  • linafilippova/admin-fe
  • Exilat_a_Tolosa/admin-fe
  • mkljczk/admin-fe
  • maxf/admin-fe
  • kphrx/admin-fe
  • vaartis/admin-fe
  • ELR/admin-fe
  • eugenijm/admin-fe
  • jp/admin-fe
  • mkfain/admin-fe
  • lorenzoancora/admin-fe
  • alexgleason/admin-fe
  • seanking/admin-fe
  • ilja/admin-fe
15 results
Show changes
Showing
with 2436 additions and 788 deletions
/*
* SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
* SPDX-License-Identifier: AGPL-3.0-only
*/
@mixin settings {
a {
text-decoration: underline;
}
.center-label label {
text-align: center;
span {
float: left;
}
}
.code {
background-color: #adbed67a;
border-radius: 3px;
font-family: monospace;
padding: 0 3px 0 3px;
}
.delete-setting-button {
margin-left: 5px;
}
.description-container {
overflow-wrap: break-word;
.el-form-item__content {
line-height: 20px;
}
}
.divider {
margin: 0 0 18px 0;
}
.divider.thick-line {
height: 2px;
}
.docs-search-container {
display: flex;
justify-content: flex-end;
margin-right: 30px;
}
.editable-keyword-container {
width: 100%;
}
.el-form-item .rate-limit {
margin-right: 0;
}
.el-input-group__prepend {
padding-left: 10px;
padding-right: 10px;
}
.el-tabs__header {
z-index: 2002;
}
.email-address-input {
width: 50%;
margin-right: 10px;
}
.esshd-list {
margin: 0;
}
.expl, .expl > p {
color: #666666;
font-size: 13px;
line-height: 22px;
margin: 5px 0 0 0;
overflow-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
code {
display: inline;
line-height: 22px;
font-size: 13px;
padding: 2px 3px;
}
}
.form-container {
margin-bottom: 80px;
}
.frontend-container {
margin-right: 30px;
}
.frontend-form-input {
margin-top: 20px;
}
.frontends-button-container {
width: 100%;
margin-top: 15px;
}
.frontends-table {
width: 100%;
margin-right: 30px;
}
.grouped-settings-header {
margin: 0 0 14px 0;
}
.highlight {
background-color: #e6e6e6;
}
.icons-button-container {
width: 100%;
margin-bottom: 10px;
}
.icons-button-desc {
font-size: 14px;
color: #606266;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei";
margin-left: 5px;
}
.icon-container {
flex-direction: column;
width: 95%;
}
.icon-values-container {
display: flex;
margin: 0 10px 10px 0;
}
.icon-key-input {
width: 30%;
margin-right: 8px
}
.icon-minus-button {
width: 36px;
height: 36px;
}
.icon-value-input {
width: 70%;
margin-left: 8px;
}
.icons-container {
display: flex;
}
.input-container {
display: flex;
align-items: flex-start;
justify-content: space-between;
.el-form-item {
margin-right: 30px;
width: 100%
}
.el-select {
width: 100%;
}
}
.install-frontend-button {
margin-top: 15px;
float: right;
}
.keyword-container {
width: 100%
}
label {
overflow: hidden;
text-overflow: ellipsis;
}
.label-font {
font-size: 14px;
color: #606266;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei";
font-weight: 700;
}
.limit-button-container {
display: flex;
align-items: baseline;
}
.limit-expl {
margin-left: 10px;
}
.limit-input {
width: 47%;
margin: 0 0 5px 1%
}
.line {
width: 100%;
height: 0;
border: 1px solid #eee;
margin-bottom: 18px;
}
.mascot {
margin-bottom: 15px;
}
.mascot-container {
width: 100%;
}
.mascot-input {
margin-bottom: 7px;
}
.mascot-name-container {
display: flex;
margin-bottom: 7px;
}
.mascot-name-input {
margin-right: 10px
}
.multiple-select-container {
width: 100%;
}
.name-input {
width: 30%;
margin-right: 8px
}
.nickname-input {
width: 50%;
}
.no-top-margin {
margin-top: 0;
p {
margin-right: 30px;
}
}
.pattern-input {
width: 20%;
margin-right: 8px
}
.proxy-url-input {
display: flex;
align-items: center;
margin-bottom: 10px;
width: 100%;
}
.proxy-url-host-input {
width: 35%;
margin-right: 8px
}
.proxy-url-value-input {
width: 35%;
margin-left: 8px;
margin-right: 10px
}
.prune-options {
display: flex;
height: 36px;
align-items: baseline;
.el-radio {
margin-top: 11px;
}
}
.rate-limit {
.el-form-item__content {
width: 100%;
display: flex;
}
}
.rate-limit-container {
width: 100%;
}
.rate-limit-content {
width: 70%;
}
.rate-limit-label {
float: right;
}
.rate-limit-label-container {
font-size: 14px;
color: #606266;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei";
font-weight: 700;
height: fit-content;
width: 30%;
margin-right: 10px;
}
.reboot-button {
width: 145px;
text-align: left;
padding: 10px;
float: right;
margin: 0 30px 0 0;
}
.reboot-button-container {
width: 100%;
position: fixed;
top: 60px;
right: 0;
z-index: 2000;
}
.replacement-input {
width: 80%;
margin-left: 8px;
margin-right: 10px
}
.sender-input {
display: flex;
align-items: center;
margin-bottom: 10px;
width: 100%;
}
.scale-input {
width: 47%;
margin: 0 1% 5px 0
}
.setting-input {
display: flex;
margin-bottom: 10px;
}
.setting-label {
font-size: 14px;
color: #606266;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei";
font-weight: 700;
line-height: 20px;
margin: 0 0 14px 0;
}
.settings-container {
max-width: 1824px;
margin: auto;
.el-tabs {
margin-top: 20px
}
}
.settings-delete-button {
margin-left: 5px;
}
.settings-docs-button {
min-width: 163px;
text-align: left;
padding: 10px;
}
.settings-header {
margin: 10px 15px 15px 15px;
}
.header-sidebar-opened {
max-width: 1585px;
}
.header-sidebar-closed {
max-width: 1728px;
}
.settings-search-input {
width: 350px;
margin-left: 5px;
}
.single-input {
margin-right: 10px
}
.socks5-checkbox {
font-size: 14px;
color: #606266;
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei";
font-weight: 700;
margin-left: 10px;
}
.socks5-checkbox-container {
width: 40%;
height: 36px;
margin-right: 5px;
display: flex;
align-items: center;
}
.ssl-tls-opts {
margin: 36px 0 0 0;
}
.submit-button {
float: right;
margin: 0 30px 22px 0;
}
.submit-button-container {
width: 100%;
position: fixed;
bottom: 0px;
right: 0;
z-index: 2000;
}
.switch-input {
height: 36px;
}
.text {
line-height: 20px;
margin-right: 15px
}
.tuple-input {
margin-right: 15px;
}
.tuple-input:last-child {
margin-right: 0;
}
.tuple-input-container {
display: flex;
}
.upload-container {
display: flex;
align-items: baseline;
}
.value-input {
width: 70%;
margin-left: 8px;
margin-right: 10px
}
@media only screen and (min-width: 1824px) {
.header-sidebar-closed {
max-width: 1772px;
}
.header-sidebar-opened {
max-width: 1630px;
}
.reboot-button-container {
width: 100%;
max-width: inherit;
margin-left: auto;
margin-right: auto;
right: auto;
}
.reboot-sidebar-opened {
max-width: 1630px;
}
.reboot-sidebar-closed {
max-width: 1772px;
}
.sidebar-closed {
max-width: 1586px;
}
.sidebar-opened {
max-width: 1442px;
}
.submit-button-container {
width: 100%;
max-width: inherit;
margin-left: auto;
margin-right: auto;
right: auto;
}
}
@media only screen and (max-width:480px) {
.crontab {
width: 100%;
label {
width: 100%;
}
}
.delete-setting-button {
margin: 4px 0 0 5px;
height: 28px;
}
.delete-setting-button-container {
flex: 0 0 auto;
}
.description > p {
line-height: 18px;
margin: 0 5px 7px 15px;
code {
display: inline;
line-height: 18px;
padding: 2px 3px;
font-size: 14px;
}
}
.description-container {
margin: 0 15px 22px 15px;
}
.divider {
margin: 0 0 10px 0;
}
.divider .thick-line {
height: 2px;
}
.frontend-container {
margin: 0 15px 10px 15px;
.description-container {
margin: 0;
}
}
.frontend-form-input {
margin-top: 0;
}
h1 {
font-size: 24px;
}
.input {
flex: 1 1 auto;
}
.input-container {
width: 100%;
.el-form-item:first-child {
margin: 0;
padding: 0 15px 10px 15px;
}
.el-form-item.crontab-container:first-child {
margin: 0;
padding: 0 ;
}
.el-form-item:first-child .mascot-form-item {
padding: 0;
}
.el-form-item:first-child .rate-limit {
padding: 0;
}
.settings-delete-button {
margin-top: 4px;
float: right;
}
}
.input-row {
display: flex;
justify-content: space-between;
}
.label-with-margin {
margin-left: 15px;
}
.limit-input {
width: 45%;
}
.proxy-url-input {
flex-direction: column;
align-items: flex-start;
margin-bottom: 0;
}
.proxy-url-host-input {
width: 100%;
margin-bottom: 5px;
}
.proxy-url-value-input {
width: 100%;
margin-left: 0;
}
.prune-options {
flex-direction: column;
height: 80px;
}
.rate-limit {
.el-form-item__content {
flex-direction: column;
}
}
.rate-limit-content {
width: 100%;
}
.rate-limit-label {
float: left;
}
.rate-limit-label-container {
width: 100%;
}
.reboot-button {
margin: 0 15px 0 0;
}
.reboot-button-container {
top: 57px;
}
.scale-input {
width: 45%;
}
.settings-header {
width: fit-content;
display: inline-block;
margin: 10px 15px 15px 15px;
}
.settings-search-input {
margin: 0 15px 25px 15px;
width: stretch;
}
.socks5-checkbox-container {
width: 100%;
}
.submit-button {
margin: 0 15px 22px 0;
}
.el-input__inner {
padding: 0 5px 0 5px
}
.el-form-item__label:not(.no-top-margin) {
padding-bottom: 5px;
line-height: 22px;
margin-top: 7px;
width: 100%;
pointer-events: none;
span {
width: 100%;
display: flex;
justify-content: space-between;
align-items: baseline;
}
button {
pointer-events: auto;
}
}
.el-message {
min-width: 80%;
}
.el-message-box {
width: 80%;
}
.el-select__tags {
overflow: hidden;
}
.expl, .expl > p {
line-height: 16px;
}
.icon-key-input {
width: 40%;
margin-right: 4px
}
.icon-minus-button {
width: 28px;
height: 28px;
margin-top: 4px;
}
.icon-values-container {
margin: 0 7px 7px 0;
}
.icon-value-input {
width: 60%;
margin-left: 4px;
}
.icons-button-container {
line-height: 24px;
}
.line {
margin-bottom: 10px;
}
.mascot-form-item {
.el-form-item__label:not(.no-top-margin) {
margin: 0;
padding: 0;
}
}
.mascot-container {
margin-bottom: 5px;
}
.name-input {
width: 40%;
margin-right: 5px
}
p.expl {
line-height: 20px;
}
.pattern-input {
width: 40%;
margin-right: 4px
}
.replacement-input {
width: 60%;
margin-left: 4px;
margin-right: 5px
}
.settings-header-container {
display: flex;
justify-content: space-between;
margin-right: 15px;
}
.value-input {
width: 60%;
margin-left: 5px;
margin-right: 8px
}
}
@media only screen and (max-width:818px) and (min-width: 481px) {
.delete-setting-button {
margin: 4px 0 0 10px;
height: 28px;
}
.delete-setting-button-container {
flex: 0 0 auto;
}
.description > p {
line-height: 18px;
margin: 0 15px 10px 0;
}
.icon-minus-button {
width: 28px;
height: 28px;
margin-top: 4px;
}
.input {
flex: 1 1 auto;
}
.input-container {
.el-form-item__label {
span {
margin-left: 10px;
}
}
}
.input-row {
display: flex;
justify-content: space-between;
}
.rate-limit-content {
width: 65%;
}
.rate-limit-label-container {
width: 35%;
}
.settings-delete-button {
float: right;
}
.settings-header-container {
display: flex;
justify-content: space-between;
margin-right: 15px;
}
.settings-search-container {
display: flex;
justify-content: flex-end;
margin-right: 15px;
}
.settings-search-input {
width: 250px;
margin: 0 0 15px 15px;
}
}
}
@mixin tiptap {
.editor {
position: relative;
border-radius: 4px;
border: 1px solid #DCDFE6;
padding: 10px;
&__content {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
padding-left: 10px;
* {
caret-color: currentColor;
}
pre {
border-radius: 5px;
font-size: 0.8rem;
overflow-x: auto;
code {
display: block;
}
}
p code {
border-radius: 5px;
font-size: 0.8rem;
font-weight: bold;
}
ul,
ol {
padding-left: 1rem;
}
li > p,
li > ol,
li > ul {
margin: 0;
}
a {
color: inherit;
}
blockquote {
border-left: 3px solid rgba(#000000, 0.1);
color: rgba(#000000, 0.8);
padding-left: 0.8rem;
font-style: italic;
p {
margin: 0;
}
}
img {
max-width: 100%;
border-radius: 3px;
}
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td, th {
min-width: 1em;
border: 2px solid #dddddd;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px; top: 0; bottom: 0;
width: 4px;
z-index: 20;
background-color: #adf;
pointer-events: none;
}
}
.tableWrapper {
margin: 1em 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
}
}
.editor-form-item {
margin-right: 30px;
}
.menubar {
margin-bottom: 1rem;
transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s;
&.is-hidden {
visibility: hidden;
opacity: 0;
}
&.is-focused {
visibility: visible;
opacity: 1;
transition: visibility 0.2s, opacity 0.2s;
}
&__button {
font-weight: bold;
display: inline-flex;
background: transparent;
border: 0;
color: #000000;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: rgba(#000000, 0.05);
}
&.is-active {
background-color: rgba(#000000, 0.1);
}
}
span#{&}__button {
font-size: 13.3333px;
}
}
}
@mixin emoji {
.create-pack {
display: flex;
justify-content: space-between
}
.create-pack-button {
margin-left: 10px;
}
.emoji-header-container {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 15px 22px 15px;
}
.emoji-name-warning {
color: #666666;
font-size: 13px;
line-height: 22px;
margin: 5px 0 0 0;
overflow-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
}
.emoji-packs-header-button-container {
display: flex;
}
.emoji-packs-form {
margin-top: 15px;
}
.emoji-packs-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 10px 15px 15px 15px;
}
.emoji-packs-tabs {
margin: 0 15px 15px 15px;
}
.import-pack-button {
margin-left: 10px;
width: 30%;
max-width: 700px;
}
h1 {
margin: 0;
}
.line {
width: 100%;
height: 0;
border: 1px solid #eee;
margin-bottom: 22px;
}
.pagination {
margin: 25px 0;
text-align: center;
}
.reboot-button {
padding: 10px;
margin: 0;
width: 145px;
}
@media only screen and (min-width: 1824px) {
.emoji-packs {
max-width: 1824px;
margin: auto;
}
}
@media only screen and (max-width:480px) {
.create-pack {
height: 82px;
flex-direction: column;
}
.create-pack-button {
margin-left: 0;
}
.divider {
margin: 15px 0;
}
.el-message {
min-width: 80%;
}
.el-message-box {
width: 80%;
}
.emoji-header-container {
flex-direction: column;
align-items: flex-start;
}
.emoji-packs-form {
margin: 0 7px;
label {
padding-right: 8px;
}
.el-form-item {
margin-bottom: 15px;
}
}
.emoji-packs-header {
margin: 15px;
}
.emoji-packs-header-button-container {
height: 82px;
flex-direction: column;
.el-button+.el-button {
margin: 7px 0 0 0;
width: fit-content;
}
}
.import-pack-button {
width: 90%;
}
.reload-emoji-button {
width: fit-content;
}
}
}
<!--
SPDX-FileCopyrightText: 2017-2019 PanJiaChen <https://github.com/PanJiaChen/vue-element-admin>
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="icons-container">
<p class="warn-content">
......
// SPDX-FileCopyrightText: 2017-2019 PanJiaChen <https://github.com/PanJiaChen/vue-element-admin>
// SPDX-License-Identifier: MIT
const req = require.context('../../icons/svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys()
......
<!--
SPDX-FileCopyrightText: 2017-2019 PanJiaChen <https://github.com/PanJiaChen/vue-element-admin>
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="app-container">
<el-card class="box-card">
......
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-dropdown v-if="isPrivileged(['users_manage_activation_state', 'users_delete', 'users_manage_tags', 'users_manage_credentials'], ['admin'])" :hide-on-click="false" size="small" trigger="click" placement="top-start" @click.native.stop>
<div>
<el-button v-if="page === 'users'" type="text" class="el-dropdown-link">
{{ $t('users.moderation') }}
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-button v-if="page === 'userPage' || page === 'statusPage'" class="moderate-user-button">
<span class="moderate-user-button-container">
<span>
<i class="el-icon-edit" />
{{ $t('users.moderateUser') }}
</span>
<i class="el-icon-arrow-down el-icon--right"/>
</span>
</el-button>
</div>
<el-dropdown-menu slot="dropdown" class="moderation-dropdown-menu">
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
class="actor-type-dropdown">
<el-select v-model="actorType" :placeholder="$t('userProfile.actorType')" class="actor-type-select">
<el-option :label="$t('users.bot')" value="Service"/>
<el-option :label="$t('users.person')" value="Person"/>
</el-select>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && showAdminAction(user)"
divided
@click.native="toggleUserRight(user, 'admin')">
{{ user.roles.admin ? $t('users.revokeAdmin') : $t('users.grantAdmin') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && showAdminAction(user)"
@click.native="toggleUserRight(user, 'moderator')">
{{ user.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_activation_state'], []) && showDeactivatedButton(user.id) && page !== 'statusPage'"
:divided="showAdminAction(user)"
@click.native="toggleActivation(user)">
{{ !user.is_active ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_delete'], []) && showDeactivatedButton(user.id) && page !== 'statusPage'"
@click.native="handleDeletion(user)">
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local && !user.is_approved"
divided
@click.native="handleAccountApproval(user)">
{{ $t('users.approveAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local && !user.is_approved"
@click.native="handleAccountRejection(user)">
{{ $t('users.rejectAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local && !user.is_confirmed"
divided
@click.native="handleEmailConfirmation(user)">
{{ $t('users.confirmAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local && !user.is_confirmed"
@click.native="handleConfirmationResend(user)">
{{ $t('users.resendConfirmation') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled"
:divided="showAdminAction(user)"
:class="{ 'active-tag': user.tags.includes('mrf_tag:media-force-nsfw') }"
@click.native="toggleTag(user, 'mrf_tag:media-force-nsfw')">
{{ $t('users.forceNsfw') }}
<i v-if="user.tags.includes('mrf_tag:media-force-nsfw')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled"
:class="{ 'active-tag': user.tags.includes('mrf_tag:media-strip') }"
@click.native="toggleTag(user, 'mrf_tag:media-strip')">
{{ $t('users.stripMedia') }}
<i v-if="user.tags.includes('mrf_tag:media-strip')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled"
:class="{ 'active-tag': user.tags.includes('mrf_tag:force-unlisted') }"
@click.native="toggleTag(user, 'mrf_tag:force-unlisted')">
{{ $t('users.forceUnlisted') }}
<i v-if="user.tags.includes('mrf_tag:force-unlisted')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && tagPolicyEnabled"
:class="{ 'active-tag': user.tags.includes('mrf_tag:sandbox') }"
@click.native="toggleTag(user, 'mrf_tag:sandbox')">
{{ $t('users.sandbox') }}
<i v-if="user.tags.includes('mrf_tag:sandbox')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && user.local && tagPolicyEnabled"
:class="{ 'active-tag': user.tags.includes('mrf_tag:disable-remote-subscription') }"
@click.native="toggleTag(user, 'mrf_tag:disable-remote-subscription')">
{{ $t('users.disableRemoteSubscription') }}
<i v-if="user.tags.includes('mrf_tag:disable-remote-subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && user.local && tagPolicyEnabled"
:class="{ 'active-tag': user.tags.includes('mrf_tag:disable-any-subscription') }"
@click.native="toggleTag(user, 'mrf_tag:disable-any-subscription')">
{{ $t('users.disableAnySubscription') }}
<i v-if="user.tags.includes('mrf_tag:disable-any-subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_tags'], []) && isPrivileged([], ['admin']) && !tagPolicyEnabled"
divided
class="no-hover"
@click.native="enableTagPolicy">
{{ $t('users.enableTagPolicy') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_credentials'], []) && user.local"
divided
@click.native="getPasswordResetToken(user.nickname)">
{{ $t('users.getPasswordResetToken') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local"
@click.native="requirePasswordReset(user)">
{{ $t('users.requirePasswordReset') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin']) && user.local"
@click.native="disableMfa(user.nickname)">
{{ $t('users.disableMfa') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
name: 'ModerationDropdown',
props: {
user: {
type: Object,
default: function() {
return {}
}
},
page: {
type: String,
default: 'users'
},
statusId: {
type: String,
default: ''
}
},
computed: {
actorType: {
get() {
return this.user.actor_type
},
set(type) {
this.$store.dispatch('UpdateActorType', {
user: this.user,
type,
_userId: this.user.id,
_statusId: this.statusId
})
}
},
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
tagPolicyEnabled() {
return this.$store.state.users.mrfPolicies.includes('Pleroma.Web.ActivityPub.MRF.TagPolicy')
}
},
methods: {
disableMfa(nickname) {
this.$store.dispatch('DisableMfa', nickname)
},
isPrivileged(accepted_privileges, accepted_roles) {
const user_privileges = this.$store.getters.privileges
const user_roles = this.$store.getters.roles
return accepted_privileges.some(privilege => user_privileges.indexOf(privilege) >= 0) || accepted_roles.some(role => user_roles.indexOf(role) >= 0)
},
enableTagPolicy() {
this.$confirm(
this.$t('users.confirmEnablingTagPolicy'),
{
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: this.$t('users.enableTagPolicySuccessMessage')
})
this.$store.dispatch('EnableTagPolicy')
}).catch(() => {
this.$message({
type: 'info',
message: 'Canceled'
})
})
},
getPasswordResetToken(nickname) {
this.$emit('open-reset-token-dialog')
this.$store.dispatch('GetPasswordResetToken', nickname)
},
handleConfirmationResend(user) {
this.$store.dispatch('ResendConfirmationEmail', [user])
},
handleDeletion(user) {
this.$confirm(
this.$t('users.deleteUserConfirmation'),
{
confirmButtonText: 'Delete',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id })
}).catch(() => {
this.$message({
type: 'info',
message: 'Delete canceled'
})
})
},
handleAccountApproval(user) {
this.$store.dispatch('ApproveUsersAccount', { users: [user], _userId: user.id, _statusId: this.statusId })
},
handleAccountRejection(user) {
this.$confirm(
this.$t('users.rejectAccountConfirmation'),
{
confirmButtonText: 'Reject',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeleteUsers', { users: [user], _userId: user.id })
}).catch(() => {
this.$message({
type: 'info',
message: 'Reject canceled'
})
})
},
handleEmailConfirmation(user) {
this.$store.dispatch('ConfirmUsersEmail', { users: [user], _userId: user.id, _statusId: this.statusId })
},
requirePasswordReset(user) {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
if (!mailerEnabled) {
this.$alert(this.$t('users.mailerMustBeEnabled'), 'Error', { type: 'error' })
return
}
this.$store.dispatch('RequirePasswordReset', [user])
},
showAdminAction({ local, id }) {
return local && this.showDeactivatedButton(id)
},
showDeactivatedButton(id) {
return this.$store.state.user.id !== id
},
toggleActivation(user) {
!user.is_active
? this.$store.dispatch('ActivateUsers', { users: [user], _userId: user.id })
: this.$store.dispatch('DeactivateUsers', { users: [user], _userId: user.id })
},
toggleTag(user, tag) {
user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag, _userId: user.id, _statusId: this.statusId })
: this.$store.dispatch('AddTag', { users: [user], tag, _userId: user.id, _statusId: this.statusId })
},
toggleUserRight(user, right) {
user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right, _userId: user.id, _statusId: this.statusId })
: this.$store.dispatch('AddRight', { users: [user], right, _userId: user.id, _statusId: this.statusId })
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.el-dropdown-menu--small .el-dropdown-menu__item.el-dropdown-menu__item--divided.actor-type-dropdown:before {
margin: 0 0;
height: 0;
}
.el-dropdown-menu--small .actor-type-dropdown {
padding: 0;
}
.actor-type-select {
width: 100%;
input {
border-color: transparent;
color: #606266;
}
.el-input__inner:hover {
border-color: transparent;
background-color: #ecf5ff;
}
.el-input.is-focus {
border-color: transparent;
}
.el-input__suffix-inner {
pointer-events: none;
}
.el-select .el-input__inner:focus {
border-color: transparent;
}
.el-input.is-active .el-input__inner, .el-input__inner:focus {
border-color: transparent;
}
}
.actor-type-select .el-input.is-focus .el-input__inner {
border-color: transparent;
}
.moderate-user-button {
text-align: left;
width: 350px;
padding: 10px;
}
.moderate-user-button-container {
display: flex;
justify-content: space-between;
}
.moderation-dropdown-menu {
width: 350px;
}
@media only screen and (max-width:480px) {
.moderate-user-button {
width: 100%
}
.moderation-dropdown-menu {
width: auto;
}
}
</style>
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-dropdown size="small" trigger="click" placement="bottom-start">
<el-dropdown v-if="isPrivileged(['users_manage_invites', 'users_delete', 'users_manage_activation_state', 'users_manage_tags'], ['admin'])" size="small" trigger="click" placement="bottom-start" class="multiple-users-menu" >
<el-button v-if="isDesktop" class="actions-button">
<span class="actions-button-container">
<span>
......@@ -11,119 +16,156 @@
</el-button>
<el-dropdown-menu v-if="showDropdownForMultipleUsers" slot="dropdown">
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
class="grant-right-to-multiple-users"
@click.native="grantRightToMultipleUsers('admin')">
{{ $t('users.grantAdmin') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
@click.native="revokeRightFromMultipleUsers('admin')">
{{ $t('users.revokeAdmin') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
@click.native="grantRightToMultipleUsers('moderator')">
{{ $t('users.grantModerator') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
@click.native="revokeRightFromMultipleUsers('moderator')">
{{ $t('users.revokeModerator') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_invites'], [])"
divided
@click.native="approveAccountsForMultipleUsers">
{{ $t('users.approveAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_delete'], [])"
@click.native="rejectAccountsForMultipleUsers">
{{ $t('users.rejectAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
divided
@click.native="confirmAccountsForMultipleUsers">
{{ $t('users.confirmAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
@click.native="resendConfirmationForMultipleUsers">
{{ $t('users.resendConfirmation') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_activation_state'], [])"
divided
@click.native="activateMultipleUsers">
{{ $t('users.activateAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_manage_activation_state'], [])"
@click.native="deactivateMultipleUsers">
{{ $t('users.deactivateAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged(['users_delete'], [])"
@click.native="deleteMultipleUsers">
{{ $t('users.deleteAccounts') }}
</el-dropdown-item>
<el-dropdown-item
v-if="isPrivileged([], ['admin'])"
@click.native="requirePasswordReset">
{{ $t('users.requirePasswordReset') }}
</el-dropdown-item>
<el-dropdown-item divided class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" divided class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.forceNsfw') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('force_nsfw')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:media-force-nsfw')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('force_nsfw')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:media-force-nsfw')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.stripMedia') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('strip_media')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:media-strip')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('strip_media')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:media-strip')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.forceUnlisted') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('force_unlisted')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:force-unlisted')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('force_unlisted')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:force-unlisted')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.sandbox') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('sandbox')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:sandbox')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('sandbox')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:sandbox')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.disableRemoteSubscriptionForMultiple') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('disable_remote_subscription')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:disable-remote-subscription')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('disable_remote_subscription')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:disable-remote-subscription')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item class="no-hover">
<el-dropdown-item v-if="tagPolicyEnabled && isPrivileged(['users_manage_tags'], [])" class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.disableAnySubscriptionForMultiple') }}</span>
<el-button-group class="tag-button-group">
<el-button size="mini" @click.native="addTagForMultipleUsers('disable_any_subscription')">
<el-button size="mini" @click.native="addTagForMultipleUsers('mrf_tag:disable-any-subscription')">
{{ $t('users.apply') }}
</el-button>
<el-button size="mini" @click.native="removeTagFromMultipleUsers('disable_any_subscription')">
<el-button size="mini" @click.native="removeTagFromMultipleUsers('mrf_tag:disable-any-subscription')">
{{ $t('users.remove') }}
</el-button>
</el-button-group>
</div>
</el-dropdown-item>
<el-dropdown-item
v-if="!tagPolicyEnabled && isPrivileged([], ['admin']) && isPrivileged(['users_manage_tags'], [])"
divided
@click.native="enableTagPolicy">
{{ $t('users.enableTagPolicy') }}
</el-dropdown-item>
</el-dropdown-menu>
<el-dropdown-menu v-else slot="dropdown">
<el-dropdown-item>
<el-dropdown-item class="select-users">
{{ $t('users.selectUsers') }}
</el-dropdown-item>
</el-dropdown-menu>
......@@ -141,102 +183,126 @@ export default {
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
showDropdownForMultipleUsers() {
return this.$props.selectedUsers.length > 0
},
isDesktop() {
return this.$store.state.app.device === 'desktop'
tagPolicyEnabled() {
return this.$store.state.users.mrfPolicies.includes('Pleroma.Web.ActivityPub.MRF.TagPolicy')
}
},
methods: {
mappers() {
const applyActionToAllUsers = (filteredUsers, fn) => Promise.all(filteredUsers.map(fn))
.then(() => {
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
}).catch((err) => {
console.log(err)
return
})
const applyAction = async(users, dispatchAction) => {
await dispatchAction(users)
this.$emit('apply-action')
}
return {
grantRight: (right) => () => {
const filterUsersFn = user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const filterUsersFn = user => this.isLocalUser(user) && !user.roles[right] && this.$store.state.user.id !== user.id
const addRightFn = async(users) => await this.$store.dispatch('AddRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, addRightFn)
},
revokeRight: (right) => () => {
const filterUsersFn = user => user.local && user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const filterUsersFn = user => this.isLocalUser(user) && user.roles[right] && this.$store.state.user.id !== user.id
const deleteRightFn = async(users) => await this.$store.dispatch('DeleteRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, deleteRightFn)
},
activate: () => {
const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const filtered = this.selectedUsers.filter(user => user.nickname && !user.is_active && this.$store.state.user.id !== user.id)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', { users })
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, activateUsersFn)
},
deactivate: () => {
const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const filtered = this.selectedUsers.filter(user => user.nickname && user.is_active && this.$store.state.user.id !== user.id)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', { users })
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, deactivateUsersFn)
},
remove: () => {
const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id)
const deleteAccountFn = async(user) => await this.$store.dispatch('DeleteUser', user)
const filtered = this.selectedUsers.filter(user => user.nickname && this.$store.state.user.id !== user.id)
const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', { users })
applyActionToAllUsers(filtered, deleteAccountFn)
applyAction(filtered, deleteAccountFn)
},
addTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
addTag: (tag) => () => {
const filtered = this.selectedUsers.filter(user =>
tag === 'mrf_tag:disable-remote-subscription' || tag === 'mrf_tag:disable-any-subscription'
? this.isLocalUser(user) && !user.tags.includes(tag)
: user.nickname && !user.tags.includes(tag))
const addTagFn = async(users) => await this.$store.dispatch('AddTag', { users, tag })
applyAction(filtered, addTagFn)
},
removeTag: (tag) => async() => {
const filtered = this.selectedUsers.filter(user =>
tag === 'mrf_tag:disable-remote-subscription' || tag === 'mrf_tag:disable-any-subscription'
? this.isLocalUser(user) && user.tags.includes(tag)
: user.nickname && user.tags.includes(tag))
const removeTagFn = async(users) => await this.$store.dispatch('RemoveTag', { users, tag })
try {
await this.$store.dispatch('AddTag', { users, tag })
} catch (err) {
console.log(err)
return
}
applyAction(filtered, removeTagFn)
},
requirePasswordReset: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user))
const requirePasswordResetFn = async(users) => await this.$store.dispatch('RequirePasswordReset', users)
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
applyAction(filtered, requirePasswordResetFn)
},
removeTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && user.tags.includes(tag)
: user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
approveAccounts: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_approved)
const approveAccountFn = async(users) => await this.$store.dispatch('ApproveUsersAccount', { users })
try {
await this.$store.dispatch('RemoveTag', { users, tag })
} catch (err) {
console.log(err)
return
}
applyAction(filtered, approveAccountFn)
},
confirmAccounts: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_confirmed)
const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', { users })
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
applyAction(filtered, confirmAccountFn)
},
requirePasswordReset: () => {
this.selectedUsers.map(user => this.$store.dispatch('RequirePasswordReset', user))
resendConfirmation: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_confirmed)
const resendConfirmationFn = async(users) => await this.$store.dispatch('ResendConfirmationEmail', users)
applyAction(filtered, resendConfirmationFn)
}
}
},
isPrivileged(accepted_privileges, accepted_roles) {
const user_privileges = this.$store.getters.privileges
const user_roles = this.$store.getters.roles
return accepted_privileges.some(privilege => user_privileges.indexOf(privilege) >= 0) || accepted_roles.some(role => user_roles.indexOf(role) >= 0)
},
enableTagPolicy() {
this.$confirm(
this.$t('users.confirmEnablingTagPolicy'),
{
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: this.$t('users.enableTagPolicySuccessMessage')
})
this.$store.dispatch('EnableTagPolicy')
}).catch(() => {
this.$message({
type: 'info',
message: 'Canceled'
})
})
},
isLocalUser(user) {
return user.nickname && user.local
},
grantRightToMultipleUsers(right) {
const { grantRight } = this.mappers()
this.confirmMessage(
......@@ -301,6 +367,34 @@ export default {
removeTag(tag)
)
},
approveAccountsForMultipleUsers() {
const { approveAccounts } = this.mappers()
this.confirmMessage(
this.$t('users.approveAccountsConfirmation'),
approveAccounts
)
},
rejectAccountsForMultipleUsers() {
const { remove } = this.mappers()
this.confirmMessage(
this.$t('users.rejectAccountsConfirmation'),
remove
)
},
confirmAccountsForMultipleUsers() {
const { confirmAccounts } = this.mappers()
this.confirmMessage(
this.$t('users.confirmAccountsConfirmation'),
confirmAccounts
)
},
resendConfirmationForMultipleUsers() {
const { resendConfirmation } = this.mappers()
this.confirmMessage(
this.$t('users.resendEmailConfirmation'),
resendConfirmation
)
},
confirmMessage(message, applyAction) {
this.$confirm(message, {
confirmButtonText: this.$t('users.ok'),
......
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-dialog
:visible.sync="isVisible"
......@@ -122,7 +127,7 @@ export default {
return re.test(email)
},
validNickname(nickname) {
var re = /^[a-zA-Z\d]+$/
var re = /^[a-zA-Z\d_-]+$/
return re.test(nickname)
}
}
......@@ -139,9 +144,8 @@ export default {
.create-account-form-item-without-margin {
margin-bottom: 0px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.create-user-dialog {
width: 85%
}
......
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-dialog
v-loading="loading"
:visible="dialogOpen"
:title="$t('users.passwordResetTokenCreated')"
custom-class="password-reset-token-dialog"
@close="closeResetPasswordDialog">
<div>
<p class="password-reset-token">{{ $t('users.passwordResetTokenGenerated') }} {{ passwordResetToken }}</p>
<p>{{ $t('users.linkToResetPassword') }}
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
</p>
</div>
</el-dialog>
</template>
<script>
export default {
name: 'ResetPasswordDialog',
props: {
resetPasswordDialogOpen: {
type: Boolean,
default: false
}
},
computed: {
dialogOpen() {
return this.resetPasswordDialogOpen
},
loading() {
return this.$store.state.users.loading
},
passwordResetLink() {
return this.$store.state.users.passwordResetToken.link
},
passwordResetToken() {
return this.$store.state.users.passwordResetToken.token
}
},
methods: {
closeResetPasswordDialog() {
this.$emit('close-reset-token-dialog')
}
}
}
</script>
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-dialog
:before-close="close"
:title="$t('userProfile.securitySettings.securitySettings')"
:visible="visible"
class="security-settings-modal">
<el-form :model="securitySettingsForm" :label-width="getLabelWidth">
<el-form-item :label="$t('userProfile.securitySettings.email')">
<el-input v-model="securitySettingsForm.newEmail" :placeholder="$t('userProfile.securitySettings.inputNewEmail')"/>
</el-form-item>
<el-form-item>
<el-button
:loading="securitySettingsForm.isEmailLoading"
:disabled="!securitySettingsForm.newEmail || securitySettingsForm.newEmail === userCredentials.email"
type="primary"
class="security-settings-submit-button"
@click="updateEmail()">
{{ $t('userProfile.securitySettings.submit') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('userProfile.securitySettings.password')" class="password-input">
<el-input v-model="securitySettingsForm.newPassword" :placeholder="$t('userProfile.securitySettings.inputNewPassword')"/>
<small class="form-text">
{{ $t('userProfile.securitySettings.passwordLengthNotice', { minLength: 8 }) }}
</small>
</el-form-item>
<el-alert
:closable="false"
type="warning"
show-icon
class="password-alert">
<p>{{ $t('userProfile.securitySettings.passwordChangeWarning1') }}</p>
<p>{{ $t('userProfile.securitySettings.passwordChangeWarning2') }}</p>
</el-alert>
<el-form-item>
<el-button
:loading="securitySettingsForm.isPasswordLoading"
:disabled="securitySettingsForm.newPassword.length < 8"
type="primary"
class="security-settings-submit-button"
@click="updatePassword()">
{{ $t('userProfile.securitySettings.submit') }}
</el-button>
</el-form-item>
</el-form>
</el-dialog>
</template>
<script>
import { Message } from 'element-ui'
export default {
name: 'SecuritySettingsModal',
props: {
visible: {
type: Boolean,
default: false
},
user: {
type: Object,
default: function() {
return {}
}
}
},
data() {
return {
securitySettingsForm: {
newEmail: '',
newPassword: '',
isEmailLoading: false,
isPasswordLoading: false
}
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
getLabelWidth() {
return this.isDesktop ? '120px' : '85px'
},
userCredentials() {
return this.$store.state.userProfile.userCredentials
}
},
mounted: async function() {
await this.$store.dispatch('FetchUserCredentials', { nickname: this.user.nickname })
this.securitySettingsForm.newEmail = this.userCredentials.email
},
methods: {
async updateEmail() {
const credentials = { email: this.securitySettingsForm.newEmail }
this.securitySettingsForm.isEmailLoading = true
await this.$store.dispatch('UpdateUserCredentials', { nickname: this.user.nickname, credentials })
this.securitySettingsForm.isEmailLoading = false
Message({
message: this.$t('userProfile.securitySettings.emailUpdated'),
type: 'success',
duration: 5 * 1000
})
},
async updatePassword() {
const credentials = { password: this.securitySettingsForm.newPassword }
this.securitySettingsForm.isPasswordLoading = true
await this.$store.dispatch('UpdateUserCredentials', { nickname: this.user.nickname, credentials })
this.securitySettingsForm.isPasswordLoading = false
this.securitySettingsForm.newPassword = ''
Message({
message: this.$t('userProfile.securitySettings.passwordUpdated'),
type: 'success',
duration: 5 * 1000
})
},
close() {
this.$emit('close', true)
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.security-settings-container {
display: flex;
label {
width: 15%;
height: 36px;
}
}
.security-settings-modal {
.el-dialog__body {
padding-top: 10px;
}
.el-form-item {
margin-bottom: 15px;
}
.password-alert {
margin-bottom: 15px;
}
.password-input {
margin-bottom: 0;
}
}
.security-settings-submit-button {
float: right;
}
@media all and (max-width: 800px) {
.security-settings-modal {
.el-dialog {
width: 90%;
}
}
}
.security-settings-modal {
.el-alert .el-alert__description {
word-break: break-word;
font-size: 1em;
}
.form-text {
display: block;
margin-top: .25rem;
color: #909399;
}
}
</style>
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<el-select
v-model="value"
......@@ -6,13 +11,20 @@
multiple
class="select-field"
@change="toggleFilters">
<el-option-group :label="$t('usersFilter.byUserType')">
<el-option value="local">{{ $t('usersFilter.local') }}</el-option>
<el-option value="external">{{ $t('usersFilter.external') }}</el-option>
<el-option-group :label="$t('usersFilter.byAccountType')">
<el-option :label="$t('usersFilter.local')" value="local"/>
<el-option :label="$t('usersFilter.external')" value="external"/>
</el-option-group>
<el-option-group :label="$t('usersFilter.byStatus')">
<el-option value="active">{{ $t('usersFilter.active') }}</el-option>
<el-option value="deactivated">{{ $t('usersFilter.deactivated') }}</el-option>
<el-option :label="$t('usersFilter.active')" value="active"/>
<el-option :label="$t('usersFilter.deactivated')" value="deactivated"/>
<el-option :label="$t('usersFilter.pending')" value="need_approval"/>
<el-option :label="$t('usersFilter.unconfirmed')" value="unconfirmed"/>
</el-option-group>
<el-option-group :label="$t('usersFilter.byActorType')">
<el-option :label="$t('usersFilter.person')" value="Person"/>
<el-option :label="$t('usersFilter.bot')" value="Service"/>
<el-option :label="$t('usersFilter.application')" value="Application"/>
</el-option-group>
</el-select>
</template>
......@@ -21,7 +33,7 @@
export default {
data() {
return {
value: []
value: ['local', 'active']
}
},
computed: {
......@@ -29,29 +41,50 @@ export default {
return this.$store.state.app.device === 'desktop'
}
},
created() {
this.$store.dispatch('ToggleUsersFilter', this.$data.value)
},
methods: {
removeOppositeFilters() {
const filtersQuantity = Object.keys(this.$store.state.users.filters).length
const currentFilters = this.$data.value.slice()
const indexOfLocal = currentFilters.indexOf('local')
const indexOfExternal = currentFilters.indexOf('external')
const indexOfActive = currentFilters.indexOf('active')
const indexOfDeactivated = currentFilters.indexOf('deactivated')
if (currentFilters.length === filtersQuantity) {
return []
} else if (indexOfLocal > -1 && indexOfExternal > -1) {
const filterToRemove = indexOfLocal > indexOfExternal ? indexOfExternal : indexOfLocal
currentFilters.splice(filterToRemove, 1)
} else if (indexOfActive > -1 && indexOfDeactivated > -1) {
const filterToRemove = indexOfActive > indexOfDeactivated ? indexOfDeactivated : indexOfActive
currentFilters.splice(filterToRemove, 1)
}
return currentFilters
const currentFilters = []
const indexOfLocal = this.$data.value.indexOf('local')
const indexOfExternal = this.$data.value.indexOf('external')
const indexOfActive = this.$data.value.indexOf('active')
const indexOfDeactivated = this.$data.value.indexOf('deactivated')
const indexOfPending = this.$data.value.indexOf('need_approval')
const indexOfUnconfirmed = this.$data.value.indexOf('unconfirmed')
const indexOfPerson = this.$data.value.indexOf('Person')
const indexOfService = this.$data.value.indexOf('Service')
const indexOfApplication = this.$data.value.indexOf('Application')
Math.max(indexOfLocal, indexOfExternal) > -1
? currentFilters.push(this.$data.value[Math.max(indexOfLocal, indexOfExternal)])
: currentFilters
Math.max(indexOfActive, indexOfDeactivated, indexOfPending, indexOfUnconfirmed) > -1
? currentFilters.push(this.$data.value[Math.max(indexOfActive, indexOfDeactivated, indexOfPending, indexOfUnconfirmed)])
: currentFilters
const actorTypeFilters = [indexOfPerson, indexOfService, indexOfApplication].reduce((acc, index) => {
if (index > -1) {
currentFilters.push(this.$data.value[index])
acc.push(this.$data.value[index])
}
return acc
}, [])
return [
currentFilters,
currentFilters.filter(filter => !actorTypeFilters.includes(filter)),
actorTypeFilters
]
},
toggleFilters() {
this.$data.value = this.removeOppositeFilters()
const currentFilters = this.$data.value.reduce((acc, filter) => ({ ...acc, [filter]: true }), {})
this.$store.dispatch('ToggleUsersFilter', currentFilters)
const [allFilters, filters, actorTypeFilters] = this.removeOppositeFilters()
this.$data.value = allFilters
this.$store.dispatch('ToggleUsersFilter', filters)
this.$store.dispatch('ToggleActorTypeFilter', actorTypeFilters)
}
}
}
......@@ -61,9 +94,8 @@ export default {
.select-field {
width: 350px;
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.select-field {
width: 100%;
margin-bottom: 5px;
......
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="users-container">
<h1>
{{ $t('users.users') }}
<span class="user-count">({{ normalizedUsersCount }})</span>
</h1>
<div class="users-header-container">
<h1>
{{ $t('users.users') }}
<span class="user-count">({{ normalizedUsersCount }})</span>
</h1>
<reboot-button/>
</div>
<div class="filter-container">
<users-filter/>
<el-input :placeholder="$t('users.search')" v-model="search" class="search" @input="handleDebounceSearchInput"/>
<el-input
:placeholder="$t('users.search')"
v-model="search"
prefix-icon="el-icon-search"
class="search"
@input="handleDebounceSearchInput"/>
</div>
<div class="actions-container">
<el-button class="actions-button create-account" @click="createAccountDialogOpen = true">
<span>
<el-button v-if="isPrivileged([], ['admin'])" class="actions-button" @click="createAccountDialogOpen = true">
<span class="create-account">
<i class="el-icon-plus"/>
{{ $t('users.createAccount') }}
</span>
......@@ -29,6 +42,7 @@
:data="users"
row-key="id"
style="width: 100%"
@row-click="handleRowClick($event)"
@selection-change="handleSelectionChange">
<el-table-column
v-if="isDesktop"
......@@ -39,7 +53,7 @@
<el-table-column :min-width="width" :label="$t('users.id')" prop="id" />
<el-table-column :label="$t('users.name')" prop="nickname">
<template slot-scope="scope">
<router-link :to="{ name: 'UsersShow', params: { id: scope.row.id }}">{{ scope.row.nickname }}</router-link>
{{ scope.row.nickname }}
<el-tag v-if="isDesktop" type="info" size="mini">
<span>{{ scope.row.local ? $t('users.local') : $t('users.external') }}</span>
</el-tag>
......@@ -47,124 +61,69 @@
</el-table-column>
<el-table-column :min-width="width" :label="$t('users.status')">
<template slot-scope="scope">
<el-tag :type="scope.row.deactivated ? 'danger' : 'success'">
<span v-if="isDesktop">{{ scope.row.deactivated ? $t('users.deactivated') : $t('users.active') }}</span>
<i v-else :class="activationIcon(scope.row.deactivated)"/>
<el-tag v-if="scope.row.is_active && scope.row.is_approved" type="success">
<span v-if="isDesktop">{{ $t('users.active') }}</span>
<i v-else class="el-icon-circle-check"/>
</el-tag>
<el-tag v-if="!scope.row.is_active && scope.row.is_approved" type="danger">
<span v-if="isDesktop">{{ $t('users.deactivated') }}</span>
<i v-else class="el-icon-circle-close"/>
</el-tag>
<el-tooltip :content="$t('users.unapprovedAccount')" effect="dark">
<el-tag v-if="!scope.row.is_approved" type="info">
<span v-if="isDesktop">{{ $t('users.unapproved') }}</span>
<i v-else class="el-icon-warning-outline"/>
</el-tag>
</el-tooltip>
<el-tag v-if="scope.row.roles.admin">
<span>{{ isDesktop ? $t('users.admin') : getFirstLetter($t('users.admin')) }}</span>
</el-tag>
<el-tag v-if="scope.row.roles.moderator">
<span>{{ isDesktop ? $t('users.moderator') : getFirstLetter($t('users.moderator')) }}</span>
</el-tag>
<el-tooltip :content="$t('users.unconfirmedEmail')" effect="dark">
<el-tag v-if="!scope.row.is_confirmed" type="info">
{{ isDesktop ? $t('users.unconfirmed') : getFirstLetter($t('users.unconfirmed')) }}
</el-tag>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('users.actions')" fixed="right">
<el-table-column v-if="pendingView && isDesktop" :label="$t('users.registrationReason')">
<template slot-scope="scope">
<el-dropdown size="small" trigger="click">
<span class="el-dropdown-link">
{{ $t('users.moderation') }}
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
<el-tooltip
v-if="regReason(scope.row.registration_reason)"
:content="scope.row.registration_reason"
popper-class="reason-tooltip"
effect="dark">
<span>
"{{ scope.row.registration_reason | truncate(100, '...') }}"
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="showAdminAction(scope.row)"
@click.native="toggleUserRight(scope.row, 'admin')">
{{ scope.row.roles.admin ? $t('users.revokeAdmin') : $t('users.grantAdmin') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showAdminAction(scope.row)"
@click.native="toggleUserRight(scope.row, 'moderator')">
{{ scope.row.roles.moderator ? $t('users.revokeModerator') : $t('users.grantModerator') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(scope.row.id)"
:divided="showAdminAction(scope.row)"
@click.native="handleDeactivation(scope.row)">
{{ scope.row.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(scope.row.id)"
@click.native="handleDeletion(scope.row)">
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
:divided="showAdminAction(scope.row)"
:class="{ 'active-tag': scope.row.tags.includes('force_nsfw') }"
@click.native="toggleTag(scope.row, 'force_nsfw')">
{{ $t('users.forceNsfw') }}
<i v-if="scope.row.tags.includes('force_nsfw')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': scope.row.tags.includes('strip_media') }"
@click.native="toggleTag(scope.row, 'strip_media')">
{{ $t('users.stripMedia') }}
<i v-if="scope.row.tags.includes('strip_media')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': scope.row.tags.includes('force_unlisted') }"
@click.native="toggleTag(scope.row, 'force_unlisted')">
{{ $t('users.forceUnlisted') }}
<i v-if="scope.row.tags.includes('force_unlisted')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
:class="{ 'active-tag': scope.row.tags.includes('sandbox') }"
@click.native="toggleTag(scope.row, 'sandbox')">
{{ $t('users.sandbox') }}
<i v-if="scope.row.tags.includes('sandbox')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local"
:class="{ 'active-tag': scope.row.tags.includes('disable_remote_subscription') }"
@click.native="toggleTag(scope.row, 'disable_remote_subscription')">
{{ $t('users.disableRemoteSubscription') }}
<i v-if="scope.row.tags.includes('disable_remote_subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local"
:class="{ 'active-tag': scope.row.tags.includes('disable_any_subscription') }"
@click.native="toggleTag(scope.row, 'disable_any_subscription')">
{{ $t('users.disableAnySubscription') }}
<i v-if="scope.row.tags.includes('disable_any_subscription')" class="el-icon-check"/>
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local"
divided
@click.native="getPasswordResetToken(scope.row.nickname)">
{{ $t('users.getPasswordResetToken') }}
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local"
@click.native="requirePasswordReset(scope.row.nickname)">
{{ $t('users.requirePasswordReset') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="$t('users.actions')" fixed="right">
<template slot-scope="scope">
<moderation-dropdown
v-if="propertyExists(scope.row, 'nickname')"
:user="scope.row"
:page="'users'"
@open-reset-token-dialog="openResetPasswordDialog"/>
<el-button v-else type="text" disabled>
{{ $t('users.moderation') }}
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-loading="loading"
:visible.sync="resetPasswordDialogOpen"
:title="$t('users.passwordResetTokenCreated')"
custom-class="password-reset-token-dialog"
@close="closeResetPasswordDialog">
<div>
<p class="password-reset-token">Password reset token was generated: {{ passwordResetToken }}</p>
<p>You can also use this link to reset password:
<a :href="passwordResetLink" target="_blank" class="reset-password-link">{{ passwordResetLink }}</a>
</p>
</div>
</el-dialog>
<div v-if="users.length === 0" class="no-users-message">
<p>There are no users to display</p>
</div>
<reset-password-dialog
:reset-password-dialog-open="resetPasswordDialogOpen"
@close-reset-token-dialog="closeResetPasswordDialog"/>
<div v-if="!loading" class="pagination">
<el-pagination
:total="usersCount"
:current-page="currentPage"
:page-size="pageSize"
background
hide-on-single-page
layout="prev, pager, next"
@current-change="handlePageChange"
/>
......@@ -178,13 +137,24 @@ import numeral from 'numeral'
import UsersFilter from './components/UsersFilter'
import MultipleUsersMenu from './components/MultipleUsersMenu'
import NewAccountDialog from './components/NewAccountDialog'
import ModerationDropdown from './components/ModerationDropdown'
import RebootButton from '@/components/RebootButton'
import ResetPasswordDialog from './components/ResetPasswordDialog'
export default {
name: 'Users',
components: {
UsersFilter,
NewAccountDialog,
ModerationDropdown,
MultipleUsersMenu,
NewAccountDialog
RebootButton,
ResetPasswordDialog,
UsersFilter
},
filters: {
truncate: function(text, length, suffix) {
return text.length < length ? text : text.substring(0, length) + suffix
}
},
data() {
return {
......@@ -201,21 +171,9 @@ export default {
normalizedUsersCount() {
return numeral(this.$store.state.users.totalUsersCount).format('0a')
},
users() {
return this.$store.state.users.fetchedUsers
},
usersCount() {
return this.$store.state.users.totalUsersCount
},
pageSize() {
return this.$store.state.users.pageSize
},
passwordResetLink() {
return this.$store.state.users.passwordResetToken.link
},
passwordResetToken() {
return this.$store.state.users.passwordResetToken.token
},
currentPage() {
return this.$store.state.users.currentPage
},
......@@ -225,6 +183,15 @@ export default {
isMobile() {
return this.$store.state.app.device === 'mobile'
},
users() {
return this.$store.state.users.fetchedUsers
},
usersCount() {
return this.$store.state.users.totalUsersCount
},
pendingView() {
return this.$store.state.users.filters.includes('need_approval')
},
width() {
return this.isMobile ? 55 : false
}
......@@ -235,52 +202,33 @@ export default {
}, 500)
},
mounted: function() {
this.$store.dispatch('NeedReboot')
this.$store.dispatch('FetchTagPolicySetting')
this.$store.dispatch('FetchUsers', { page: 1 })
},
destroyed() {
this.$store.dispatch('ClearUsersState')
},
methods: {
activationIcon(status) {
return status ? 'el-icon-error' : 'el-icon-success'
},
clearSelection() {
this.$refs.usersTable.clearSelection()
},
isPrivileged(accepted_privileges, accepted_roles) {
const user_privileges = this.$store.getters.privileges
const user_roles = this.$store.getters.roles
return accepted_privileges.some(privilege => user_privileges.indexOf(privilege) >= 0) || accepted_roles.some(role => user_roles.indexOf(role) >= 0)
},
closeResetPasswordDialog() {
this.resetPasswordDialogOpen = false
this.$store.dispatch('RemovePasswordToken')
},
async createNewAccount(accountData) {
try {
await this.$store.dispatch('CreateNewAccount', accountData)
} catch (_e) {
return
} finally {
this.createAccountDialogOpen = false
}
this.$message({
type: 'success',
message: this.$t('users.accountCreated')
})
await this.$store.dispatch('CreateNewAccount', accountData)
this.createAccountDialogOpen = false
},
getFirstLetter(str) {
return str.charAt(0).toUpperCase()
},
getPasswordResetToken(nickname) {
this.resetPasswordDialogOpen = true
this.$store.dispatch('GetPasswordResetToken', nickname)
},
requirePasswordReset(nickname) {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
if (!mailerEnabled) {
this.$alert(this.$t('users.mailerMustBeEnabled'), 'Error', { type: 'error' })
return
}
this.$store.dispatch('RequirePasswordReset', { nickname })
},
handleDeactivation({ nickname }) {
this.$store.dispatch('ToggleUserActivation', nickname)
},
handleDeletion(user) {
this.$store.dispatch('DeleteUser', user)
},
handlePageChange(page) {
const searchQuery = this.$store.state.users.searchQuery
if (searchQuery === '') {
......@@ -289,26 +237,25 @@ export default {
this.$store.dispatch('SearchUsers', { query: searchQuery, page })
}
},
handleRowClick(row) {
if (row.id) {
this.$router.push({ name: 'UsersShow', params: { id: row.id }})
}
},
handleSelectionChange(value) {
this.$data.selectedUsers = value
},
closeResetPasswordDialog() {
this.resetPasswordDialogOpen = false
this.$store.dispatch('RemovePasswordToken')
openResetPasswordDialog() {
this.resetPasswordDialogOpen = true
},
showAdminAction({ local, id }) {
return local && this.showDeactivatedButton(id)
propertyExists(account, property) {
return account[property]
},
regReason(reason) {
return reason && reason.length > 0
},
showDeactivatedButton(id) {
return this.$store.state.user.id !== id
},
toggleTag(user, tag) {
user.tags.includes(tag)
? this.$store.dispatch('RemoveTag', { users: [user], tag })
: this.$store.dispatch('AddTag', { users: [user], tag })
},
toggleUserRight(user, right) {
this.$store.dispatch('ToggleRight', { user, right })
}
}
}
......@@ -326,6 +273,9 @@ export default {
justify-content: space-between;
align-items: center;
margin: 0 15px 10px 15px;
.el-dropdown {
margin-left: 10px;
}
}
.active-tag {
color: #409EFF;
......@@ -336,11 +286,16 @@ export default {
margin: 7px 0 0 15px;
}
}
.active-tag.is-disabled {
.el-icon-check {
color: #bbb;
}
}
.el-dropdown-link:hover {
cursor: pointer;
color: #409EFF;
}
.el-icon-plus {
.create-account > .el-icon-plus {
margin-right: 5px;
}
.password-reset-token {
......@@ -349,58 +304,77 @@ export default {
.password-reset-token-dialog {
width: 50%
}
.reason-tooltip {
max-width: 450px;
}
.reset-password-link {
text-decoration: underline;
}
.users-header-container {
display: flex;
align-items: center;
justify-content: space-between;
}
.users-container {
h1 {
margin: 22px 0 0 15px;
margin: 10px 0 0 15px;
height: 40px;
}
.cell {
word-break: break-word;
}
.el-table__row:hover {
cursor: pointer;
}
.pagination {
margin: 25px 0;
text-align: center;
}
.reboot-button {
margin: 0 15px 0 0;
padding: 10px;
width: 145px;
}
.search {
width: 350px;
float: right;
margin-left: 10px;
}
.filter-container {
display: flex;
height: 36px;
justify-content: space-between;
align-items: center;
margin: 22px 15px 15px 15px
margin: 15px
}
.user-count {
color: gray;
font-size: 28px;
}
}
@media
only screen and (max-width: 760px),
(min-device-width: 768px) and (max-device-width: 1024px) {
@media only screen and (max-width:480px) {
.password-reset-token-dialog {
width: 85%
}
.users-container {
h1 {
margin: 7px 10px 15px 10px;
margin: 0;
}
.actions-button {
width: 100%;
}
.actions-container {
display: flex;
flex-direction: column;
margin: 0 10px 7px 10px
}
.create-account {
width: 100%;
}
.el-icon-arrow-down {
font-size: 12px;
}
.search {
width: 100%;
margin-left: 0;
}
.filter-container {
display: flex;
......@@ -408,18 +382,35 @@ only screen and (max-width: 760px),
flex-direction: column;
margin: 0 10px
}
.el-tag {
width: 30px;
display: inline-block;
margin-bottom: 4px;
font-weight: bold;
&.el-tag--success {
padding-left: 8px;
}
&.el-tag--danger {
padding-left: 8px;
.el-table__row {
.el-tag {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
margin-bottom: 4px;
font-weight: bold;
}
}
.reboot-button {
margin: 0;
}
.users-header-container {
margin: 7px 10px 12px 10px;
}
.user-count {
color: gray;
font-size: 22px;
}
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.actions-button {
width: 49%;
}
.search {
width: 49%;
}
}
</style>
<!--
SPDX-FileCopyrightText: 2019-2022 Pleroma Authors <https://pleroma.social>
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<main v-if="!loading">
<header>
<el-avatar :src="user.avatar" size="large" />
<h1>{{ user.display_name }}</h1>
<main v-if="!userProfileLoading">
<header v-if="isDesktop || isTablet" class="user-page-header">
<div class="avatar-name-container">
<el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" />
<h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1>
<h1 v-else class="invalid">({{ $t('users.invalidNickname') }})</h1>
<a v-if="propertyExists(user, 'url')" :href="user.url" target="_blank">
<i :title="$t('userProfile.openAccountInInstance')" class="el-icon-top-right"/>
</a>
</div>
<div class="left-header-container">
<moderation-dropdown
v-if="propertyExists(user, 'nickname')"
:user="user"
:page="'userPage'"
@open-reset-token-dialog="openResetPasswordDialog"/>
<reboot-button/>
</div>
</header>
<el-row>
<el-col :span="6">
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
<table class="el-table__body">
<tbody>
<tr class="el-table__row">
<td class="name-col">ID</td>
<td class="value-col">
{{ user.id }}
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.tags') }}</td>
<td>
<el-tag v-for="tag in user.tags" :key="tag">{{ tag }}</el-tag>
<span v-if="user.tags.length === 0">None</span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.moderator') }}</td>
<td>
<el-tag v-if="user.roles.moderator" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.roles.moderator" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.admin') }}</td>
<td>
<el-tag v-if="user.roles.admin" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.roles.admin" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.local') }}</td>
<td>
<el-tag v-if="user.local" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.local" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.deactivated') }}</td>
<td>
<el-tag v-if="user.deactivated" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.deactivated" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.nickname') }}</td>
<td>
{{ user.nickname }}
</td>
</tr>
</tbody>
</table>
<div v-if="isMobile" class="user-page-header-container">
<header class="user-page-header">
<div class="avatar-name-container">
<el-avatar v-if="propertyExists(user, 'avatar')" :src="user.avatar" size="large" />
<h1 v-if="propertyExists(user, 'nickname')">{{ user.nickname }}</h1>
<h1 v-else class="invalid">({{ $t('users.invalidNickname') }})</h1>
</div>
</el-col>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="18"><h2>{{ $t('userProfile.recentStatuses') }}</h2></el-col>
<el-col :span="6" class="show-private">
<el-checkbox v-model="showPrivate" @change="onTogglePrivate">
{{ $t('userProfile.showPrivateStatuses') }}
</el-checkbox>
</el-col>
</el-row>
<el-col :span="18">
<el-timeline class="statuses">
<el-timeline-item v-for="status in statuses" :timestamp="createdAtLocaleString(status.created_at)" :key="status.id">
<el-card>
<strong v-if="status.spoiler_text">{{ status.spoiler_text }}</strong>
<p v-if="status.content" v-html="status.content" />
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</div>
</el-card>
<reboot-button/>
</header>
<moderation-dropdown
v-if="propertyExists(user, 'nickname')"
:user="user"
:page="'userPage'"
@open-reset-token-dialog="openResetPasswordDialog"/>
</div>
<reset-password-dialog
:reset-password-dialog-open="resetPasswordDialogOpen"
@close-reset-token-dialog="closeResetPasswordDialog"/>
<div class="user-profile-container">
<div class="user-cards-container">
<el-card class="user-profile-card">
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
<el-tag v-if="!propertyExists(user, 'nickname')" type="info" class="invalid-user-tag">
{{ $t('users.invalidAccount') }}
</el-tag>
<table class="user-profile-table">
<tbody>
<tr class="el-table__row">
<td class="name-col">ID</td>
<td>
{{ user.id }}
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.actorType') }}</td>
<td>
<el-tag
:type="userCredentials.actor_type === 'Person' ? 'success' : 'warning'">
{{ userCredentials.actor_type }}
</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.tags') }}</td>
<td>
<span v-if="user.tags.length === 0 || !propertyExists(user, 'tags')"></span>
<el-tag v-for="tag in user.tags" v-else :key="tag" class="user-profile-tag">{{ humanizeTag(tag) }}</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.roles') }}</td>
<td>
<el-tag v-if="user.roles.admin" class="user-profile-tag">
{{ $t('users.admin') }}
</el-tag>
<el-tag v-if="user.roles.moderator" class="user-profile-tag">
{{ $t('users.moderator') }}
</el-tag>
<span v-if="!propertyExists(user, 'roles') || (!user.roles.moderator && !user.roles.admin)"></span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.accountType') }}</td>
<td>
<el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag>
<el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.status') }}</td>
<td>
<el-tag v-if="!user.is_approved" type="info">{{ $t('userProfile.pending') }}</el-tag>
<el-tag v-if="user.is_active && user.is_approved" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="!user.is_active" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
</td>
</tr>
</tbody>
</table>
<div v-if="user.registration_reason">
<div class="reason-label">{{ $t('userProfile.reason') }}</div>
"{{ user.registration_reason }}"
</div>
</div>
<el-button v-if="propertyExists(user, 'nickname')" icon="el-icon-lock" class="security-setting-button" @click="securitySettingsModalVisible = true">
{{ $t('userProfile.securitySettings.securitySettings') }}
</el-button>
<SecuritySettingsModal
v-if="propertyExists(user, 'nickname')"
:user="user"
:visible="securitySettingsModalVisible"
@close="securitySettingsModalVisible = false" />
</el-card>
<el-card class="user-chats-card">
<h2 class="chats">{{ $t('userProfile.chats') }}</h2>
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
<table class="user-chats-table">
<tbody v-if="!chatsLoading" class="chats">
<tr v-if="chats.length === 0" class="no-statuses">
{{ $t('userProfile.noChats') }}
</tr>
<tr v-for="chat in chats" :key="chat.id" class="el-table__row chat-item">
<td>
<router-link
v-if="propertyExists(chat, 'id')"
:to="{ name: 'ChatsShow', params: { id: chat.id }}"
class="router-link">
<div class="chat-card-header">
<img v-if="propertyExists(chat.receiver, 'avatar')" :src="chat.receiver.avatar" class="chat-avatar-img">
<span v-if="propertyExists(chat.receiver, 'username')" class="chat-account-name">{{ chat.receiver.username }}</span>
<span v-else>
<span v-if="propertyExists(chat.receiver, 'username')" class="chat-account-name">
{{ chat.receiver.username }}
</span>
<span v-else class="chat-account-name deactivated">({{ $t('users.invalidNickname') }})</span>
</span>
</div>
<div class="chat-card-preview">
<span v-if="propertyExists(chat, 'last_message')" class="chat-preview">{{ chat.last_message.content }}</span>
</div>
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</el-card>
</div>
<div class="recent-statuses-container">
<h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }}</h2>
<el-checkbox v-model="showPrivate" class="show-private-statuses" @change="onTogglePrivate">
{{ $t('statuses.showPrivateStatuses') }}
</el-checkbox>
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :account="status.account" :show-checkbox="false" :user-id="user.id" :godmode="showPrivate"/>
</el-timeline-item>
<p v-if="statuses.length === 0" class="no-statuses">{{ $t('userProfile.noStatuses') }}</p>
</el-timeline>
</el-col>
</el-row>
</div>
</div>
</main>
</template>
<script>
import Status from '@/components/Status'
import ModerationDropdown from './components/ModerationDropdown'
import SecuritySettingsModal from './components/SecuritySettingsModal'
import RebootButton from '@/components/RebootButton'
import ResetPasswordDialog from './components/ResetPasswordDialog'
export default {
name: 'UsersShow',
components: { ModerationDropdown, RebootButton, ResetPasswordDialog, Status, SecuritySettingsModal },
data() {
return {
showPrivate: false
showPrivate: false,
resetPasswordDialogOpen: false,
securitySettingsModalVisible: false
}
},
computed: {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isMobile() {
return this.$store.state.app.device === 'mobile'
},
isTablet() {
return this.$store.state.app.device === 'tablet'
},
loading() {
return this.$store.state.userProfile.loading
return this.$store.state.users.loading
},
statuses() {
return this.$store.state.userProfile.statuses
},
statusesLoading() {
return this.$store.state.userProfile.statusesLoading
},
chats() {
return this.$store.state.userProfile.chats
},
chatsLoading() {
return this.$store.state.userProfile.chatsLoading
},
user() {
return this.$store.state.userProfile.user
},
statuses() {
return this.$store.state.userProfile.statuses
userProfileLoading() {
return this.$store.state.userProfile.userProfileLoading
},
userCredentials() {
return this.$store.state.userProfile.userCredentials
}
},
mounted: function() {
this.$store.dispatch('FetchData', { id: this.$route.params.id, godmode: false })
this.$store.dispatch('NeedReboot')
this.$store.dispatch('GetNodeInfo')
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: false })
},
methods: {
optionPercent(poll, pollOption) {
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
if (allVotes === 0) {
return 0
}
return +(pollOption.votes_count / allVotes * 100).toFixed(1)
closeResetPasswordDialog() {
this.resetPasswordDialogOpen = false
this.$store.dispatch('RemovePasswordToken')
},
createdAtLocaleString(createdAt) {
const date = new Date(createdAt)
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
humanizeTag(tag) {
const mapTags = {
'mrf_tag:media-force-nsfw': 'Force NSFW',
'mrf_tag:media-strip': 'Strip Media',
'mrf_tag:force-unlisted': 'Force Unlisted',
'mrf_tag:sandbox': 'Sandbox',
'mrf_tag:disable-remote-subscription': 'Disable remote subscription',
'mrf_tag:disable-any-subscription': 'Disable any subscription'
}
return mapTags[tag]
},
onTogglePrivate() {
console.log(this.showPrivate)
this.$store.dispatch('FetchData', { id: this.$route.params.id, godmode: this.showPrivate })
this.$store.dispatch('FetchUserProfile', { userId: this.$route.params.id, godmode: this.showPrivate })
},
openResetPasswordDialog() {
this.resetPasswordDialogOpen = true
},
propertyExists(account, property) {
return account[property]
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss' scoped>
<style rel='stylesheet/scss' lang='scss'>
header {
align-items: center;
display: flex;
......@@ -154,26 +268,231 @@ table {
width: 150px;
}
}
.avatar-name-container {
display: flex;
align-items: center;
.el-icon-top-right {
font-size: 2em;
line-height: 36px;
color: #606266;
}
}
.invalid {
color: gray;
}
.el-table--border::after, .el-table--group::after, .el-table::before {
background-color: transparent;
}
.poll ul {
list-style-type: none;
padding: 0;
width: 30%;
}
.image {
width: 20%;
img {
width: 100%;
}
}
.invalid-user-tag {
font-size: 14px;
width: inherit;
height: auto;
text-align: center;
word-wrap: break-word;
white-space: normal;
}
.left-header-container {
align-items: center;
display: flex;
justify-content: space-between;
}
.no-statuses {
margin-left: 28px;
color: #606266;
}
.password-reset-token {
margin: 0 0 14px 0;
}
.password-reset-token-dialog {
width: 50%
}
.poll ul {
list-style-type: none;
padding: 0;
width: 30%;
}
.reboot-button {
padding: 10px;
margin-left: 10px;
}
.recent-statuses-container {
display: flex;
flex-direction: column;
width: 67%;
}
.recent-statuses-header {
margin-top: 10px;
}
.reset-password-link {
text-decoration: underline;
}
.security-setting-button {
margin-top: 20px;
width: 100%;
}
.statuses {
padding-right: 20px;
padding: 0 20px 0 0;
}
.show-private {
text-align: right;
width: 200px;
text-align: left;
line-height: 67px;
padding-right: 20px;
margin-right: 20px;
}
.show-private-statuses {
margin-left: 28px;
margin-bottom: 20px;
}
.recent-statuses {
margin-left: 28px;
}
.user-page-header {
display: flex;
justify-content: space-between;
margin: 22px 15px 22px 20px;
padding: 0;
align-items: center;
h1 {
display: inline
}
}
.user-cards-container {
display: flex;
flex-direction: column;
width: 30%;
min-width: 300px;
margin: 0 20px;
}
.user-profile-card {
height: fit-content;
width: auto;
margin-bottom: 20px;
}
.user-chats-card {
width: auto;
height: fit-content;
margin-bottom: 20px;
}
.user-profile-container {
display: flex;
}
.user-profile-table {
margin: 0;
width: inherit;
}
.user-chats-table {
width: 100%;
}
.user-profile-tag {
margin: 0 4px 4px 0;
}
.reason-label {
color: #878d99;
font-weight: bold;
margin: 5px 0;
}
.chat-card-header {
display: flex;
align-items: center;
}
.chat-avatar-img {
display: inline-block;
width: 15px;
height: 15px;
margin-right: 5px;
}
.chat-account-name {
display: inline-block;
margin: 0;
font-size: 15px;
font-weight: 500;
}
.chat-card-preview {
color: gray;
font-style: italic;
margin: 5px 0 0 20px;
}
@media only screen and (max-width:480px) {
.avatar-name-container {
margin-bottom: 10px;
}
.el-timeline-item__wrapper {
padding-left: 18px;
}
.password-reset-token-dialog {
width: 85%
}
.recent-statuses {
margin: 20px 10px 15px 10px;
}
.recent-statuses-container {
width: 100%;
margin: 0;
}
.show-private-statuses {
margin: 0 10px 20px 10px;
}
.status-container {
margin: 0 10px;
}
.statuses {
padding-right: 10px;
margin-left: 8px;
}
.user-page-header {
padding: 0;
margin: 7px 15px 15px 10px;
}
.user-page-header-container {
.el-dropdown {
width: 95%;
margin: 0 15px 15px 10px;
}
}
.user-profile-card, .user-chats-card {
margin: 0 10px 20px;
width: 95%;
td {
width: 80px;
}
}
.user-profile-container {
flex-direction: column;
}
.user-cards-container {
width: 100%;
margin: 0;
}
}
@media only screen and (max-width:801px) and (min-width: 481px) {
.recent-statuses {
margin: 20px 10px 15px 0;
}
.recent-statuses-container {
width: 97%;
margin: 0 20px;
}
.show-private-statuses {
margin: 0 10px 20px 0;
}
.user-page-header {
padding: 0;
margin: 7px 15px 20px 20px;
}
.user-profile-container {
flex-direction: column;
}
.user-cards-container {
width: 66%;
padding-left: 28px;
}
}
</style>
tinymce.addI18n('zh_CN',{
"Cut": "\u526a\u5207",
"Heading 5": "\u6807\u98985",
"Header 2": "\u6807\u98982",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u5bf9\u526a\u8d34\u677f\u7684\u8bbf\u95ee\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u952e\u8fdb\u884c\u590d\u5236\u7c98\u8d34\u3002",
"Heading 4": "\u6807\u98984",
"Div": "Div\u533a\u5757",
"Heading 2": "\u6807\u98982",
"Paste": "\u7c98\u8d34",
"Close": "\u5173\u95ed",
"Font Family": "\u5b57\u4f53",
"Pre": "\u9884\u683c\u5f0f\u6587\u672c",
"Align right": "\u53f3\u5bf9\u9f50",
"New document": "\u65b0\u6587\u6863",
"Blockquote": "\u5f15\u7528",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Heading 1": "\u6807\u98981",
"Headings": "\u6807\u9898",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Formats": "\u683c\u5f0f",
"Headers": "\u6807\u9898",
"Select all": "\u5168\u9009",
"Header 3": "\u6807\u98983",
"Blocks": "\u533a\u5757",
"Undo": "\u64a4\u6d88",
"Strikethrough": "\u5220\u9664\u7ebf",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Header 1": "\u6807\u98981",
"Superscript": "\u4e0a\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Font Sizes": "\u5b57\u53f7",
"Subscript": "\u4e0b\u6807",
"Header 6": "\u6807\u98986",
"Redo": "\u91cd\u590d",
"Paragraph": "\u6bb5\u843d",
"Ok": "\u786e\u5b9a",
"Bold": "\u7c97\u4f53",
"Code": "\u4ee3\u7801",
"Italic": "\u659c\u4f53",
"Align center": "\u5c45\u4e2d",
"Header 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Heading 3": "\u6807\u98983",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Header 4": "\u6807\u98984",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Underline": "\u4e0b\u5212\u7ebf",
"Cancel": "\u53d6\u6d88",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Inline": "\u6587\u672c",
"Copy": "\u590d\u5236",
"Align left": "\u5de6\u5bf9\u9f50",
"Visual aids": "\u7f51\u683c\u7ebf",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Square": "\u65b9\u5757",
"Default": "\u9ed8\u8ba4",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"Name": "\u540d\u79f0",
"Anchor": "\u951a\u70b9",
"Id": "\u6807\u8bc6\u7b26",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Source code": "\u6e90\u4ee3\u7801",
"Language": "\u8bed\u8a00",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"B": "B",
"R": "R",
"G": "G",
"Color": "\u989c\u8272",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Emoticons": "\u8868\u60c5",
"Robots": "\u673a\u5668\u4eba",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Encoding": "\u7f16\u7801",
"Description": "\u63cf\u8ff0",
"Author": "\u4f5c\u8005",
"Fullscreen": "\u5168\u5c4f",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Source": "\u5730\u5740",
"Border": "\u8fb9\u6846",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Style": "\u6837\u5f0f",
"Dimensions": "\u5927\u5c0f",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image": "\u56fe\u7247",
"Zoom in": "\u653e\u5927",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Back": "\u540e\u9000",
"Gamma": "\u4f3d\u9a6c\u503c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Sharpen": "\u9510\u5316",
"Zoom out": "\u7f29\u5c0f",
"Image options": "\u56fe\u7247\u9009\u9879",
"Apply": "\u5e94\u7528",
"Brightness": "\u4eae\u5ea6",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Crop": "\u88c1\u526a",
"Orientation": "\u65b9\u5411",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Invert": "\u53cd\u8f6c",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Url": "\u5730\u5740",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Anchors": "\u951a\u70b9",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Link": "\u94fe\u63a5",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"None": "\u65e0",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Media": "\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Poster": "\u5c01\u9762",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Embed": "\u5185\u5d4c",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Print": "\u6253\u5370",
"Save": "\u4fdd\u5b58",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Replace": "\u66ff\u6362",
"Next": "\u4e0b\u4e00\u4e2a",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Replace with": "\u66ff\u6362\u4e3a",
"Find": "\u67e5\u627e",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Prev": "\u4e0a\u4e00\u4e2a",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Finish": "\u5b8c\u6210",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Ignore": "\u5ffd\u7565",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Rows": "\u884c",
"Height": "\u9ad8",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Column group": "\u5217\u7ec4",
"Row": "\u884c",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Row type": "\u884c\u7c7b\u578b",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Body": "\u8868\u4f53",
"Caption": "\u6807\u9898",
"Footer": "\u8868\u5c3e",
"Delete row": "\u5220\u9664\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Scope": "\u8303\u56f4",
"Delete table": "\u5220\u9664\u8868\u683c",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Column": "\u5217",
"Row group": "\u884c\u7ec4",
"Cell": "\u5355\u5143\u683c",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Copy row": "\u590d\u5236\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Header": "\u8868\u5934",
"Right": "\u53f3\u5bf9\u9f50",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Cols": "\u5217",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Width": "\u5bbd",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Left": "\u5de6\u5bf9\u9f50",
"Cut row": "\u526a\u5207\u884c",
"Delete column": "\u5220\u9664\u5217",
"Center": "\u5c45\u4e2d",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Insert template": "\u63d2\u5165\u6a21\u677f",
"Templates": "\u6a21\u677f",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Text color": "\u6587\u5b57\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"Insert": "\u63d2\u5165",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Tools": "\u5de5\u5177",
"View": "\u89c6\u56fe",
"Table": "\u8868\u683c",
"Format": "\u683c\u5f0f"
});
\ No newline at end of file
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif

354 B

static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif

329 B

static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif

340 B

static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif

336 B