Add moderation modal
This commit is contained in:
parent
2e00c19074
commit
4fe11ee378
14 changed files with 396 additions and 25 deletions
|
@ -5,6 +5,7 @@ import FeaturesPanel from './components/features_panel/features_panel.vue'
|
|||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import ModModal from './components/mod_modal/mod_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
|
@ -33,6 +34,7 @@ export default {
|
|||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
ModModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
<EditStatusModal v-if="editingAvailable" />
|
||||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<ModModal />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -16,7 +16,8 @@ import {
|
|||
faUsers,
|
||||
faCommentMedical,
|
||||
faBookmark,
|
||||
faInfoCircle
|
||||
faInfoCircle,
|
||||
faUserTie
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
@ -34,7 +35,8 @@ library.add(
|
|||
faUsers,
|
||||
faCommentMedical,
|
||||
faBookmark,
|
||||
faInfoCircle
|
||||
faInfoCircle,
|
||||
faUserTie
|
||||
)
|
||||
|
||||
export default {
|
||||
|
@ -109,6 +111,9 @@ export default {
|
|||
},
|
||||
openSettingsModal () {
|
||||
this.$store.dispatch('openSettingsModal')
|
||||
},
|
||||
openModModal () {
|
||||
this.$store.dispatch('openModModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,6 +151,18 @@
|
|||
:title="$t('nav.preferences')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin' || currentUser.role === 'moderator'"
|
||||
class="button-unstyled nav-icon"
|
||||
@click.stop="openModModal"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="user-tie"
|
||||
:title="$t('nav.moderation')"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
href="/pleroma/admin/#/login-pleroma"
|
||||
|
|
58
src/components/mod_modal/mod_modal.js
Normal file
58
src/components/mod_modal/mod_modal.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import Modal from 'src/components/modal/modal.vue'
|
||||
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faChevronDown
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faWindowMinimize
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faWindowMinimize,
|
||||
faChevronDown
|
||||
)
|
||||
|
||||
const ModModal = {
|
||||
components: {
|
||||
Modal,
|
||||
ModModalContent: getResettableAsyncComponent(
|
||||
() => import('./mod_modal_content.vue'),
|
||||
{
|
||||
loadingComponent: PanelLoading,
|
||||
errorComponent: AsyncComponentError,
|
||||
delay: 0
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeModModal')
|
||||
},
|
||||
peekModal () {
|
||||
this.$store.dispatch('togglePeekModModal')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
moderator () {
|
||||
return this.$store.state.users.currentUser &&
|
||||
(this.$store.state.users.currentUser.role === 'admin' ||
|
||||
this.$store.state.users.currentUser.role === 'moderator')
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
modalOpenedOnce () {
|
||||
return this.$store.state.interface.modModalLoaded
|
||||
},
|
||||
modalPeeked () {
|
||||
return this.$store.state.interface.modModalState === 'minimized'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModal
|
63
src/components/mod_modal/mod_modal.scss
Normal file
63
src/components/mod_modal/mod_modal.scss
Normal file
|
@ -0,0 +1,63 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod-modal {
|
||||
overflow: hidden;
|
||||
|
||||
.setting-list,
|
||||
.option-list {
|
||||
list-style-type: none;
|
||||
padding-left: 2em;
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.suboptions {
|
||||
margin-top: 0.3em
|
||||
}
|
||||
}
|
||||
|
||||
&.peek {
|
||||
.mod-modal-panel {
|
||||
/* Explanation:
|
||||
* Modal is positioned vertically centered.
|
||||
* 100vh - 100% = Distance between modal's top+bottom boundaries and screen
|
||||
* (100vh - 100%) / 2 = Distance between bottom (or top) boundary and screen
|
||||
* + 100% - we move modal completely off-screen, it's top boundary touches
|
||||
* bottom of the screen
|
||||
* - 50px - leaving tiny amount of space so that titlebar + tiny amount of modal is visible
|
||||
*/
|
||||
transform: translateY(calc(((100vh - 100%) / 2 + 100%) - 50px));
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
/* For mobile, the modal takes 100% of the available screen.
|
||||
This ensures the minimized modal is always 50px above the browser bottom bar regardless of whether or not it is visible.
|
||||
*/
|
||||
transform: translateY(calc(100% - 50px));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mod-modal-panel {
|
||||
overflow: hidden;
|
||||
transition: transform;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 300ms;
|
||||
width: 1000px;
|
||||
max-width: 90vw;
|
||||
height: 90vh;
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
max-width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
>.panel-body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
.btn {
|
||||
min-height: 2em;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
src/components/mod_modal/mod_modal.vue
Normal file
43
src/components/mod_modal/mod_modal.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="moderator"
|
||||
:is-open="modalActivated"
|
||||
class="mod-modal"
|
||||
:class="{ peek: modalPeeked }"
|
||||
:no-background="modalPeeked"
|
||||
>
|
||||
<div class="mod-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
<span class="title">
|
||||
{{ $t('moderation.moderation') }}
|
||||
</span>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.peek')"
|
||||
@click="peekModal"
|
||||
>
|
||||
<FAIcon
|
||||
:icon="['far', 'window-minimize']"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
:title="$t('general.close')"
|
||||
@click="closeModal"
|
||||
>
|
||||
<FAIcon
|
||||
icon="times"
|
||||
fixed-width
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<ModModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal.js"></script>
|
||||
<style src="./mod_modal.scss" lang="scss"></style>
|
63
src/components/mod_modal/mod_modal_content.js
Normal file
63
src/components/mod_modal/mod_modal_content.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||
|
||||
import ReportsTab from './tabs/reports_tab.vue'
|
||||
import StatusesTab from './tabs/statuses_tab.vue'
|
||||
import UsersTab from './tabs/users_tab.vue'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFlag,
|
||||
faMessage,
|
||||
faUsers
|
||||
)
|
||||
|
||||
const ModModalContent = {
|
||||
components: {
|
||||
TabSwitcher,
|
||||
|
||||
ReportsTab,
|
||||
StatusesTab,
|
||||
UsersTab
|
||||
},
|
||||
computed: {
|
||||
open () {
|
||||
return this.$store.state.interface.modModalState !== 'hidden'
|
||||
},
|
||||
bodyLock () {
|
||||
return this.$store.state.interface.modModalState === 'visible'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onOpen () {
|
||||
const targetTab = this.$store.state.interface.modModalTargetTab
|
||||
// We're being told to open in specific tab
|
||||
if (targetTab) {
|
||||
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||
})
|
||||
if (tabIndex >= 0) {
|
||||
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||
}
|
||||
}
|
||||
// Clear the state of target tab, so that next time moderation is opened
|
||||
// it doesn't force it.
|
||||
this.$store.dispatch('clearModModalTargetTab')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.onOpen()
|
||||
},
|
||||
watch: {
|
||||
open: function (value) {
|
||||
if (value) this.onOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ModModalContent
|
54
src/components/mod_modal/mod_modal_content.scss
Normal file
54
src/components/mod_modal/mod_modal_content.scss
Normal file
|
@ -0,0 +1,54 @@
|
|||
@import 'src/_variables.scss';
|
||||
.mod_tab-switcher {
|
||||
height: 100%;
|
||||
|
||||
.setting-item {
|
||||
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||
margin: 1em 1em 1.4em;
|
||||
padding-bottom: 1.4em;
|
||||
|
||||
> div,
|
||||
> label {
|
||||
display: block;
|
||||
margin-bottom: .5em;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-multiple {
|
||||
display: flex;
|
||||
|
||||
.option-list {
|
||||
margin: 0;
|
||||
padding-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
select {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.unavailable,
|
||||
.unavailable svg {
|
||||
color: var(--cRed, $fallback--cRed);
|
||||
color: $fallback--cRed;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
max-width: 6em;
|
||||
}
|
||||
}
|
||||
}
|
34
src/components/mod_modal/mod_modal_content.vue
Normal file
34
src/components/mod_modal/mod_modal_content.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
class="mod_tab-switcher"
|
||||
:side-tab-bar="true"
|
||||
:scrollable-tabs="true"
|
||||
:body-scroll-lock="bodyLock"
|
||||
>
|
||||
<div
|
||||
:label="$t('moderation.reports')"
|
||||
icon="flag"
|
||||
data-tab-name="reports"
|
||||
>
|
||||
<ReportsTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('moderation.users')"
|
||||
icon="users"
|
||||
data-tab-name="users"
|
||||
>
|
||||
<UsersTab />
|
||||
</div>
|
||||
<div
|
||||
:label="$t('moderation.statuses')"
|
||||
icon="message"
|
||||
data-tab-name="statuses"
|
||||
>
|
||||
<StatusesTab />
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</template>
|
||||
|
||||
<script src="./mod_modal_content.js"></script>
|
||||
<style src="./mod_modal_content.scss" lang="scss"></style>
|
|
@ -64,8 +64,12 @@ export default {
|
|||
settingsModalVisible () {
|
||||
return this.settingsModalState === 'visible'
|
||||
},
|
||||
modModalVisible () {
|
||||
return this.modModalState === 'visible'
|
||||
},
|
||||
...mapState({
|
||||
settingsModalState: state => state.interface.settingsModalState
|
||||
settingsModalState: state => state.interface.settingsModalState,
|
||||
modModalState: state => state.interface.modModalState
|
||||
})
|
||||
},
|
||||
beforeUpdate () {
|
||||
|
|
|
@ -266,6 +266,12 @@
|
|||
"next": "Next",
|
||||
"previous": "Previous"
|
||||
},
|
||||
"moderation": {
|
||||
"moderation": "Moderation",
|
||||
"reports": "Reports",
|
||||
"statuses": "Statuses",
|
||||
"users": "Users"
|
||||
},
|
||||
"nav": {
|
||||
"about": "About",
|
||||
"administration": "Administration",
|
||||
|
|
|
@ -2,6 +2,9 @@ const defaultState = {
|
|||
settingsModalState: 'hidden',
|
||||
settingsModalLoaded: false,
|
||||
settingsModalTargetTab: null,
|
||||
modModalState: 'hidden',
|
||||
modModalLoaded: false,
|
||||
modModalTargetTab: null,
|
||||
settings: {
|
||||
currentSaveStateNotice: null,
|
||||
noticeClearTimeout: null,
|
||||
|
@ -63,6 +66,30 @@ const interfaceMod = {
|
|||
setSettingsModalTargetTab (state, value) {
|
||||
state.settingsModalTargetTab = value
|
||||
},
|
||||
closeModModal (state) {
|
||||
state.modModalState = 'hidden'
|
||||
},
|
||||
togglePeekModModal (state) {
|
||||
switch (state.modModalState) {
|
||||
case 'minimized':
|
||||
state.modModalState = 'visible'
|
||||
return
|
||||
case 'visible':
|
||||
state.modModalState = 'minimized'
|
||||
return
|
||||
default:
|
||||
throw new Error('Illegal minimization state of mod modal')
|
||||
}
|
||||
},
|
||||
openModModal (state) {
|
||||
state.modModalState = 'visible'
|
||||
if (!state.modModalLoaded) {
|
||||
state.modModalLoaded = true
|
||||
}
|
||||
},
|
||||
setModModalTargetTab (state, value) {
|
||||
state.modModalTargetTab = value
|
||||
},
|
||||
pushGlobalNotice (state, notice) {
|
||||
state.globalNotices.push(notice)
|
||||
},
|
||||
|
@ -105,6 +132,18 @@ const interfaceMod = {
|
|||
commit('setSettingsModalTargetTab', value)
|
||||
commit('openSettingsModal')
|
||||
},
|
||||
closeModModal ({ commit }) {
|
||||
commit('closeModModal')
|
||||
},
|
||||
openModModal ({ commit }) {
|
||||
commit('openModModal')
|
||||
},
|
||||
togglePeekModModal ({ commit }) {
|
||||
commit('togglePeekModModal')
|
||||
},
|
||||
clearModModalTargetTab ({ commit }) {
|
||||
commit('setModModalTargetTab', null)
|
||||
},
|
||||
pushGlobalNotice (
|
||||
{ commit, dispatch, state },
|
||||
{
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -1092,12 +1092,12 @@
|
|||
|
||||
"@fortawesome/fontawesome-common-types@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893"
|
||||
integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w==
|
||||
|
||||
"@fortawesome/fontawesome-svg-core@1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.3.0.tgz#343fac91fa87daa630d26420bfedfba560f85885"
|
||||
integrity sha512-UIL6crBWhjTNQcONt96ExjUnKt1D68foe3xjEensLDclqQ6YagwCRYVQdrp/hW0ALRp/5Fv/VKw+MqTUWYYvPg==
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-common-types" "^0.3.0"
|
||||
|
@ -1141,9 +1141,9 @@
|
|||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@intlify/bundle-utils@next":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.1.2.tgz"
|
||||
integrity sha512-amgSo0NN5OKWYdcgFmfJqo2tcUcZ6C66Bxm5ALQnB0m3MUQtS9aJzKoIo+EU9XQiOVmlBFxRtNoZm+psHa5FNA==
|
||||
version "3.2.1"
|
||||
resolved "https://registry.npmjs.org/@intlify/bundle-utils/-/bundle-utils-3.2.1.tgz"
|
||||
integrity sha512-rf4cLBOnbqmpXVcCdcYHilZpMt1m82syh3WLBJlZvGxN2KkH9HeHVH4+bnibF/SDXCHNh6lM6wTpS/qw+PkcMg==
|
||||
dependencies:
|
||||
"@intlify/message-compiler" next
|
||||
"@intlify/shared" next
|
||||
|
@ -1168,7 +1168,7 @@
|
|||
dependencies:
|
||||
"@intlify/shared" "9.2.2"
|
||||
|
||||
"@intlify/message-compiler@9.2.2":
|
||||
"@intlify/message-compiler@9.2.2", "@intlify/message-compiler@next":
|
||||
version "9.2.2"
|
||||
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz"
|
||||
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
|
||||
|
@ -1176,24 +1176,11 @@
|
|||
"@intlify/shared" "9.2.2"
|
||||
source-map "0.6.1"
|
||||
|
||||
"@intlify/message-compiler@next":
|
||||
version "9.3.0-beta.3"
|
||||
resolved "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.3.0-beta.3.tgz"
|
||||
integrity sha512-j8OwToBQgs01RBMX4GCDNQfcnmw3AiDG3moKIONTrfXcf+1yt/rWznLTYH/DXbKcFMAFijFpCzMYjUmH1jVFYA==
|
||||
dependencies:
|
||||
"@intlify/shared" "9.3.0-beta.3"
|
||||
source-map "0.6.1"
|
||||
|
||||
"@intlify/shared@9.2.2", "@intlify/shared@next":
|
||||
version "9.2.2"
|
||||
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz"
|
||||
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
|
||||
|
||||
"@intlify/shared@9.3.0-beta.3":
|
||||
version "9.3.0-beta.3"
|
||||
resolved "https://registry.npmjs.org/@intlify/shared/-/shared-9.3.0-beta.3.tgz"
|
||||
integrity sha512-Z/0TU4GhFKRxKh+0RbwJExik9zz57gXYgxSYaPn7YQdkQ/pabSioCY/SXnYxQHL6HzULF5tmqarFm6glbGqKhw==
|
||||
|
||||
"@intlify/vue-devtools@9.2.2":
|
||||
version "9.2.2"
|
||||
resolved "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz"
|
||||
|
@ -8095,9 +8082,9 @@ mute-stream@0.0.7:
|
|||
integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==
|
||||
|
||||
nan@^2.12.1:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
|
||||
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
|
||||
version "2.17.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||
|
||||
nanoid@^3.3.4:
|
||||
version "3.3.4"
|
||||
|
|
Loading…
Reference in a new issue