Made things do things.

This commit is contained in:
Karolis2011
2022-01-26 19:53:41 +02:00
parent 0b19ee33a8
commit bc4bc8eba7
21 changed files with 683 additions and 90 deletions

View File

@@ -13,6 +13,7 @@
"axios": "^0.25.0",
"bootstrap": "^5.1.3",
"bootstrap-icons": "^1.7.2",
"lodash": "^4.17.21",
"vue": "^3.2.25",
"vue-router": "^4.0.12",
"vuex": "^4.0.2"

View File

@@ -1,6 +1,11 @@
<template>
<nav-menu />
<header>
<nav-menu />
<admin-menu />
</header>
<data-alert />
<confirm-alert ref="ca" />
<div class="mb-3"></div>
<div v-if="isLocal" class="container">
<div class="alert alert-danger">
<h4 class="alert-heading">Lokali sistemą aptikta</h4>
@@ -20,20 +25,26 @@
</span>
</div>
</div>
<transition name="slide-fade">
<router-view />
</transition>
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<script>
import NavMenu from './components/NavMenu.vue'
import DataAlert from './components/DataAlert.vue'
import ConfirmAlert from './components/ConfirmAlert.vue'
import AdminMenu from './components/AdminMenu.vue'
export default {
name: 'App',
components: {
NavMenu,
DataAlert,
ConfirmAlert,
AdminMenu,
},
data() {
return {
@@ -54,6 +65,36 @@ export default {
return location.protocol !== 'https:'
},
},
methods: {
async confirm(text, title, options = {}) {
const passedOptions = Object.assign(
{ text, title, affirmitive: {}, negative: {} },
options
)
await new Promise((res, rej) => {
passedOptions.affirmitive.action = res
passedOptions.negative.action = () =>
rej(new Error('User rejected confirmation.'))
this.$refs.ca.configure(passedOptions)
this.$refs.ca.show()
})
},
async deleteConfirmation() {
await this.confirm(
'Ar tikrai norite ištrinti šį elementą?',
'Ištrinimo patvirtinimas',
{
affirmitive: {
text: 'Ištrinti',
type: 'danger',
},
negative: {
text: 'Atšaukti',
},
}
)
},
},
}
</script>

View File

@@ -7,17 +7,11 @@
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
transition: all 0.3s ease-out;
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-leave-active,
.slide-fade-enter-active {
position: relative;
top: 0;
transition: all 0.15s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
@@ -31,3 +25,23 @@
transform: translateX(0px);
opacity: 1;
}
.fade-enter-active {
transition: all 0.3s ease-out;
}
.fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: scale(0);
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: scale(1);
}

View File

@@ -0,0 +1,58 @@
<template>
<nav
v-if="$store.state.msalAuth.isAdmin"
class="navbar navbar-expand-lg navbar-dark bg-black"
>
<div class="container-fluid">
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#adminbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="adminbarNav"
>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link :to="{ name: 'Issues' }" class="nav-link"
>Pateiktos problemos</router-link
>
</li>
<li class="nav-item">
<router-link :to="{ name: 'AdminIssueTypes' }" class="nav-link"
>Problemų tipai</router-link
>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'AdminMenu',
data() {
return {
isExpanded: false,
}
},
methods: {
collapse() {
this.isExpanded = false
},
toggle() {
this.isExpanded = !this.isExpanded
},
},
}
</script>

View File

@@ -0,0 +1,67 @@
<template>
<transition name="fade">
<div
v-if="shown"
class="alert"
:class="{ ['alert-' + type]: true }"
role="alert"
>
<h4 v-if="title" class="alert-heading">{{ title }}</h4>
{{ text }}
</div>
</transition>
</template>
<script>
import defaults from 'lodash/defaults'
const defaultOptions = {
title: null,
text: 'You have not configured alerter.',
type: 'danger',
}
export default {
data() {
return {
shown: false,
text: 'A simple alert',
title: null,
type: 'primary',
dismisstimer: null,
}
},
methods: {
show() {
this.shown = true
},
hide() {
this.shown = false
this.cancelAutoDismiss()
},
cancelAutoDismiss() {
if (this.dismisstimer) clearTimeout(this.dismisstimer)
this.dismisstimer = null
},
autoDismiss(timeout) {
this.cancelAutoDismiss()
this.dismisstimer = setTimeout(() => this.hide(), timeout)
},
configure(options) {
const final = defaults(options, defaultOptions)
this.text = final.text
this.title = final.title
this.type = final.type
},
alert(options) {
this.configure(options)
this.cancelAutoDismiss()
this.show()
},
alertTimed(options, timeout = 4000) {
this.alert(options)
this.autoDismiss(timeout)
},
},
}
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ options.title }}</h5>
</div>
<div class="modal-body">
<p>
{{ options.text }}
</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
:class="'btn-' + options.affirmitive.type"
@click="accept"
>
{{ options.affirmitive.text }}
</button>
<button
type="button"
class="btn"
:class="'btn-' + options.negative.type"
@click="reject"
>
{{ options.negative.text }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import defaultsDeep from 'lodash/defaultsDeep'
import { Modal } from 'bootstrap'
const defaultValues = {
title: 'Confirmation',
text: 'Do you confirm this action?',
affirmitive: {
hide: false,
text: 'Yes',
type: 'primary',
action: () => null,
},
negative: {
hide: false,
text: 'No',
type: 'secondary',
action: () => null,
},
}
export default {
data() {
return {
modal: null,
beenUsed: false,
options: Object.assign({}, defaultValues),
}
},
mounted() {
var el = this.$refs.modal
el.addEventListener('hide.bs.modal', () => this.handleHideEvent())
this.modal = new Modal(el, {})
},
methods: {
handleHideEvent() {
if (!this.beenUsed) {
this.options.negative.action()
}
},
show() {
this.beenUsed = false
this.modal.show()
},
hide() {
this.modal.hide()
},
accept() {
this.beenUsed = true
this.options.affirmitive.action()
this.hide()
},
reject() {
this.beenUsed = true
this.options.negative.action()
this.hide()
},
configure(options) {
this.options = defaultsDeep({}, options || {}, defaultValues)
},
},
}
</script>

View File

@@ -1,73 +1,68 @@
<template>
<header class="mb-3">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<router-link :to="{ name: 'Home' }" class="navbar-brand"
>KTU SA Problemų sprendimo sistema</router-link
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="navbarNav"
>
<ul class="navbar-nav me-auto">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<router-link :to="{ name: 'Home' }" class="navbar-brand"
>KTU SA Problemų sprendimo sistema</router-link
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{ show: isExpanded }"
id="navbarNav"
>
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link :to="{ name: 'Home' }" class="nav-link"
>Pagrindinis</router-link
>
</li>
<template v-if="$store.state.msalAuth.isLoggedIn">
<li class="nav-item">
<router-link :to="{ name: 'Home' }" class="nav-link"
>Pagrindinis</router-link
>
</li>
<li class="nav-item" v-if="$store.state.msalAuth.isLoggedIn">
<router-link :to="{ name: 'Submit' }" class="nav-link"
>Pateikti problemą</router-link
>
</li>
<li class="nav-item" v-if="$store.state.msalAuth.isLoggedIn">
<li class="nav-item">
<router-link :to="{ name: 'Problems' }" class="nav-link"
>Problemos</router-link
>
</li>
<li class="nav-item" v-if="$store.state.msalAuth.isLoggedIn">
<li class="nav-item">
<router-link :to="{ name: 'Feedbacks' }" class="nav-link"
>Atsiliepimai</router-link
>
</li>
<li class="nav-item" v-if="$store.state.msalAuth.isAdmin">
<router-link :to="{ name: 'Issues' }" class="nav-link"
>Pateiktos problemos</router-link
>
</li>
<li v-if="$root.isLocal" class="nav-item">
<a href="/swagger" class="nav-link">Swagger UI</a>
</li>
</ul>
<div class="navbar-nav">
<template v-if="$store.state.msalAuth.isLoggedIn">
<span class="navbar-text"
>Prisijungta kaip {{ $store.state.msalAuth.displayName }}
</span>
<div class="nav-item">
<a href="#" @click="LogoutMsal" class="nav-link">Atsijungti</a>
</div>
</template>
<div v-else class="nav-item">
<a href="#" @click="LoginMsal" class="nav-link">Prisijungti</a>
</template>
<li v-if="$root.isLocal" class="nav-item">
<a href="/swagger" class="nav-link">SUI</a>
</li>
</ul>
<div class="navbar-nav">
<template v-if="$store.state.msalAuth.isLoggedIn">
<span class="navbar-text"
>Prisijungta kaip {{ $store.state.msalAuth.displayName }}
</span>
<div class="nav-item">
<a href="#" @click="LogoutMsal" class="nav-link">Atsijungti</a>
</div>
</template>
<div v-else class="nav-item">
<a href="#" @click="LoginMsal" class="nav-link">Prisijungti</a>
</div>
</div>
</div>
</nav>
</header>
</div>
</nav>
</template>
<script>

View File

@@ -0,0 +1,40 @@
<template>
<div class="mb-3">
<label for="feedbackLtTextArea" class="form-label"
>Lietuviškas Aprašymas</label
>
<textarea
v-model="feedbackLt"
class="form-control"
id="feedbackLtTextArea"
rows="4"
></textarea>
</div>
<div class="mb-3">
<label for="feedbackEnTextArea" class="form-label"
>Angliškas Aprašymas</label
>
<textarea
v-model="feedbackEn"
class="form-control"
id="feedbackEnTextArea"
rows="4"
></textarea>
</div>
</template>
<script>
import { mapModel } from './formHelpers'
export default {
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
computed: {
...mapModel(['feedbackLt', 'feedbackEn']),
},
}
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="mb-3">
<label for="nameInput" class="form-label">Lietuviškas pavadinimas</label>
<input v-model="name" type="text" class="form-control" id="nameInput" />
</div>
<div class="mb-3">
<label for="nameEnInput" class="form-label">Angliškas pavadinimas</label>
<input v-model="nameEn" type="text" class="form-control" id="nameEnInput" />
</div>
</template>
<script>
import { mapModel } from './formHelpers'
export default {
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
computed: {
...mapModel(['name', 'nameEn']),
},
}
</script>

View File

@@ -0,0 +1,27 @@
export function autoComputed(propName, defaultValue) {
return {
get() {
return this.modelValue[propName] || defaultValue
},
set(value) {
this.$emit('update:modelValue', {
...this.modelValue,
[propName]: value,
})
},
}
}
export function mapModel(props) {
const computedValues = {}
if (Array.isArray(props)) {
props.forEach((propName) => {
computedValues[propName] = autoComputed(propName, '')
})
} else {
Object.entries(props).forEach(([propName, defaultValue]) => {
computedValues[propName] = autoComputed(propName, defaultValue)
})
}
return computedValues
}

View File

@@ -11,4 +11,4 @@ const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
app.mount('#app')

View File

@@ -21,6 +21,7 @@ const msalState = {
displayName: null,
debugFullTokenResponse: null,
msalRefreshTimer: null,
}
async function initializeMSAL() {
@@ -35,6 +36,7 @@ async function initializeMSAL() {
redirectUri: window.location.protocol + '//' + window.location.host + '/',
},
}
msalState.msalRefreshTimer = setInterval(__refreshToken, 10 * 60 * 1000)
msalState.msal = new msal.PublicClientApplication(msalConfig)
@@ -70,6 +72,22 @@ export function LogoutMsal() {
msalState.msal.logout()
}
async function __refreshToken() {
if (!msalState.isLoggedIn) return
msalState.debugFullTokenResponse = await msalState.msal
.acquireTokenSilent({ scopes: RequestedScopes })
.catch((error) => {
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return msalState.msal.acquireTokenRedirect({
scopes: RequestedScopes,
})
}
})
__responseObjectToMsalState()
__stateChanged()
}
async function __handleResponse(response) {
if (response !== null) {
if (__isAccountAceptable(response.account)) {
@@ -82,7 +100,7 @@ async function __handleResponse(response) {
msalState.msal
.getAllAccounts()
.filter(__isAccountAceptable)
.forEach(account => {
.forEach((account) => {
msalState.msal.setActiveAccount(account)
})
@@ -90,7 +108,7 @@ async function __handleResponse(response) {
if (account != null) {
msalState.debugFullTokenResponse = await msalState.msal
.acquireTokenSilent({ scopes: RequestedScopes })
.catch(error => {
.catch((error) => {
if (error instanceof msal.InteractionRequiredAuthError) {
// fallback to interaction when silent call fails
return msalState.msal.acquireTokenRedirect({
@@ -118,7 +136,7 @@ function __isAccountAceptable(account) {
}
function __stateChanged() {
msalState.stateChangeCallbacks.forEach(cb => cb())
msalState.stateChangeCallbacks.forEach((cb) => cb())
}
async function __loadAuthParameters() {

View File

@@ -66,6 +66,7 @@ export default {
this.error = null
this.ok = null
try {
await this.$root.deleteConfirmation()
await authAxios.delete(`/api/Issues/${id}`)
this.ok = 'Sėkmingai ištrintą'
await this.fetchData()

View File

@@ -24,28 +24,7 @@
</div>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<div class="mb-3">
<label for="problemLtTextArea" class="form-label"
>Lietuviškas Aprašymas</label
>
<textarea
v-model="feedback.feedbackLt"
class="form-control"
id="problemLtTextArea"
rows="4"
></textarea>
</div>
<div class="mb-3">
<label for="problemEnTextArea" class="form-label"
>Angliškas Aprašymas</label
>
<textarea
v-model="feedback.feedbackEn"
class="form-control"
id="problemEnTextArea"
rows="4"
></textarea>
</div>
<feedback-form v-model="feedback" />
<button @click="create" class="btn btn-primary btn-lg">
Sukurti naują atsiliepimą
</button>
@@ -56,8 +35,12 @@
<script>
import { axios, authAxios } from '@/axios'
import FeedbackForm from '@/components/forms/FeedbackForm.vue'
export default {
components: {
FeedbackForm,
},
data() {
return {
issue: null,

View File

@@ -0,0 +1,15 @@
<template>
<div>
<div class="container">
<div class="alert alert-warning">
<b>Dėmesio!</b> Tu esi administracinėje skiltyje, visą informacija
pateikiama čia yra galimai konfidianciali.
</div>
</div>
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>

View File

@@ -0,0 +1,62 @@
<template>
<div class="container">
<h1>Redaguoti problemos tipą</h1>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<issue-type-form v-model="issueType" />
<alerter ref="submitAlerter" />
<button @click="update" class="btn btn-primary btn-lg">
Išsaugoti
</button>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
import IssueTypeForm from '@/components/forms/IssueTypeForm.vue'
import Alerter from '@/components/Alerter.vue'
export default {
components: {
IssueTypeForm,
Alerter,
},
data() {
return {
issueType: { name: '', nameEn: '' },
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get(
`/api/IssueTypes/${this.$route.params.id}`
)
this.issueType = response.data
},
async update() {
try {
if (!this.issueType.name || !this.issueType.nameEn) {
this.$refs.submitAlerter.alertTimed({
text: 'Problemos tipas turi turėti pavadinimus!!!',
})
return
}
await authAxios.patch(
`/api/IssueTypes/${this.$route.params.id}`,
this.issueType
)
this.$router.push({ name: 'AdminIssueTypes' })
} catch (error) {
this.$refs.submitAlerter.alert({
title: 'Įvyko klaidą sukuriant tipą.',
text: error,
})
}
},
},
}
</script>

View File

@@ -0,0 +1,54 @@
<template>
<div class="container">
<h1>Naujas problemos tipas</h1>
<div class="row justify-content-center">
<div class="card col-lg-6 p-5">
<issue-type-form v-model="issueType" />
<alerter ref="submitAlerter" />
<button @click="create" class="btn btn-primary btn-lg">
Sukurti naują problemos tipą
</button>
</div>
</div>
</div>
</template>
<script>
import { authAxios } from '@/axios'
import IssueTypeForm from '@/components/forms/IssueTypeForm.vue'
import Alerter from '@/components/Alerter.vue'
export default {
components: {
IssueTypeForm,
Alerter,
},
data() {
return {
issueType: { name: '', nameEn: '' },
}
},
methods: {
async create() {
try {
if (!this.issueType.name || !this.issueType.nameEn) {
this.$refs.submitAlerter.alertTimed({
text: 'Problemos tipas turi turėti pavadinimus!!!',
})
return
}
await authAxios.post('/api/IssueTypes', this.issueType)
this.$refs.submitAlerter.alertTimed({
text: 'Sėkmingai sukurtas problemos tipas.',
type: 'success',
})
this.issueType = {}
} catch (error) {
this.$refs.submitAlerter.alert({
title: 'Įvyko klaidą sukuriant tipą.',
text: error,
})
}
},
},
}
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="container">
<router-link
:to="{ name: 'AdminIssueTypeNew' }"
class="btn btn-primary mx-1"
>Sukurti naują problemos tipą</router-link
>
<div class="card my-4" v-for="it in issueTypes" :key="it.id">
<div class="row g-0">
<div class="card-body">
<h5 class="card-title">{{ it.name }}</h5>
<h5 class="card-title text-muted">{{ it.nameEn }}</h5>
<button class="btn btn-danger mx-1" @click="deleteIssueType(it.id)">
<i class="bi bi-trash-fill"></i>
Ištrinti
</button>
<router-link
:to="{ name: 'AdminIssueTypeEdit', params: { id: it.id } }"
class="btn btn-primary mx-1"
>Redaguoti</router-link
>
</div>
</div>
</div>
</div>
</template>
<script>
import { axios, authAxios } from '@/axios'
export default {
data() {
return {
issueTypes: [],
}
},
created() {
this.fetchData()
},
methods: {
async fetchData() {
const response = await axios.get('/api/IssueTypes')
this.issueTypes = response.data
},
async deleteIssueType(id) {
try {
await this.$root.deleteConfirmation()
await authAxios.delete(`/api/IssueTypes/${id}`)
// TODO: Display some success info
await this.fetchData()
} catch (error) {
// TODO: Display error.
}
},
},
}
</script>

View File

@@ -0,0 +1,28 @@
import IssueTypes from '@/pages/admin/IssueTypes.vue'
import IssueTypeNew from '@/pages/admin/IssueTypeNew.vue'
import IssueTypeEdit from '@/pages/admin/IssueTypeEdit.vue'
import Admin from '@/pages/admin/Admin.vue'
export default [
{
path: '/admin',
component: Admin,
children: [
{
path: 'issuetypes',
name: 'AdminIssueTypes',
component: IssueTypes,
},
{
path: 'issuetypes/new',
name: 'AdminIssueTypeNew',
component: IssueTypeNew,
},
{
path: 'issuetypes/:id/edit',
name: 'AdminIssueTypeEdit',
component: IssueTypeEdit,
},
],
},
]

View File

@@ -6,6 +6,7 @@ import Feedbacks from '@/pages/Feedbacks.vue'
import Issues from '@/pages/Issues.vue'
import NewProblem from '@/pages/NewProblem.vue'
import NewFeedback from '@/pages/NewFeedback.vue'
import AdminRoutes from './admin'
const routes = [
{
@@ -43,6 +44,7 @@ const routes = [
name: 'IssueNewFeedback',
component: NewFeedback,
},
...AdminRoutes,
]
const router = createRouter({

View File

@@ -375,6 +375,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"