@ -0,0 +1,12 @@ |
|||||||
|
const baseRules = { |
||||||
|
'vue/no-setup-props-destructure': 0, |
||||||
|
'no-console': 0, |
||||||
|
'antfu/if-newline': 0, |
||||||
|
'prettier/prettier': ['error', {}, { usePrettierrc: true }], |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
extends: ['@antfu', 'plugin:prettier/recommended'], |
||||||
|
plugins: ['prettier'], |
||||||
|
rules: baseRules, |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"singleQuote": true, |
||||||
|
"trailingComma": "all", |
||||||
|
"semi": false, |
||||||
|
"quoteProps": "consistent", |
||||||
|
"bracketSpacing": true, |
||||||
|
"printWidth": 130 |
||||||
|
} |
@ -1,6 +1,103 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import MdiAt from '~icons/mdi/at' |
||||||
|
import MdiLogout from '~icons/mdi/logout' |
||||||
|
import MdiDotsVertical from '~icons/mdi/dots-vertical' |
||||||
|
import MaterialSymbolsMenu from '~icons/material-symbols/menu' |
||||||
|
import { navigateTo } from '#app' |
||||||
|
|
||||||
|
const { $state } = useNuxtApp() |
||||||
|
|
||||||
|
const sidebar = ref<HTMLDivElement>() |
||||||
|
|
||||||
|
const email = computed(() => $state.user?.value?.email ?? '---') |
||||||
|
|
||||||
|
const signOut = () => { |
||||||
|
$state.signOut() |
||||||
|
navigateTo('/signin') |
||||||
|
} |
||||||
|
|
||||||
|
const toggleSidebar = useToggle($state.sidebarOpen) |
||||||
|
|
||||||
|
const sidebarOpen = computed({ |
||||||
|
get: () => !$state.sidebarOpen.value, |
||||||
|
set: (val) => toggleSidebar(val), |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<!-- <NuxtLayout>--> |
<a-layout> |
||||||
<!-- <NuxtPage />--> |
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md"> |
||||||
<!-- </NuxtLayout>--> |
<MaterialSymbolsMenu |
||||||
<NuxtPage/> |
v-if="$state.signedIn.value" |
||||||
|
class="text-xl cursor-pointer" |
||||||
|
@click="toggleSidebar(!$state.sidebarOpen.value)" |
||||||
|
/> |
||||||
|
|
||||||
|
<div class="flex-1" /> |
||||||
|
|
||||||
|
<div class="ml-4 flex justify-center flex-1"> |
||||||
|
<div class="flex items-center gap-2 cursor-pointer" @click="navigateTo('/')"> |
||||||
|
<img width="35" src="~/assets/img/icons/512x512-trans.png" /> |
||||||
|
<span class="prose-xl">NocoDB</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- todo: loading is not yet supported by nuxt 3 - see https://v3.nuxtjs.org/migration/component-options#loading |
||||||
|
<span v-show="$nuxt.$loading.show" class="caption grey--text ml-3"> |
||||||
|
{{ $t('general.loading') }} <v-icon small color="grey">mdi-spin mdi-loading</v-icon> |
||||||
|
</span> |
||||||
|
|
||||||
|
|
||||||
|
todo: replace shortkey? |
||||||
|
<span v-shortkey="['ctrl', 'shift', 'd']" @shortkey="openDiscord" /> |
||||||
|
--> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex-1" /> |
||||||
|
|
||||||
|
<div class="flex justify-end gap-4"> |
||||||
|
<general-language class="mr-3" /> |
||||||
|
|
||||||
|
<template v-if="$state.signedIn.value"> |
||||||
|
<a-dropdown :trigger="['click']"> |
||||||
|
<MdiDotsVertical class="md:text-xl cursor-pointer" @click.prevent /> |
||||||
|
|
||||||
|
<template #overlay> |
||||||
|
<a-menu class="!py-0 nc-user-menu min-w-32 dark:(!bg-gray-800) leading-8 !rounded"> |
||||||
|
<a-menu-item key="0" class="!rounded-t"> |
||||||
|
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user"> |
||||||
|
<MdiAt class="mt-1 group-hover:text-success" /> |
||||||
|
<span class="prose group-hover:text-black">{{ email }}</span> |
||||||
|
</nuxt-link> |
||||||
|
</a-menu-item> |
||||||
|
|
||||||
|
<a-menu-divider class="!m-0" /> |
||||||
|
|
||||||
|
<a-menu-item key="1" class="!rounded-b"> |
||||||
|
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="signOut"> |
||||||
|
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" /> |
||||||
|
<span class="prose font-semibold text-gray-500 group-hover:text-black">{{ $t('general.signOut') }}</span> |
||||||
|
</div> |
||||||
|
</a-menu-item> |
||||||
|
</a-menu> |
||||||
|
</template> |
||||||
|
</a-dropdown> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</a-layout-header> |
||||||
|
|
||||||
|
<a-layout> |
||||||
|
<a-layout-sider |
||||||
|
v-model:collapsed="sidebarOpen" |
||||||
|
width="300" |
||||||
|
collapsed-width="0" |
||||||
|
class="bg-white dark:!bg-gray-800 border-r-1 border-gray-200 dark:!border-gray-600 h-full" |
||||||
|
:trigger="null" |
||||||
|
collapsible |
||||||
|
> |
||||||
|
<div id="sidebar" ref="sidebar" class="w-full h-full" /> |
||||||
|
</a-layout-sider> |
||||||
|
|
||||||
|
<NuxtPage /> |
||||||
|
</a-layout> |
||||||
|
</a-layout> |
||||||
</template> |
</template> |
||||||
|
@ -0,0 +1,8 @@ |
|||||||
|
# ASSETS |
||||||
|
|
||||||
|
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. |
||||||
|
|
||||||
|
More information about the usage of this directory in the documentation: |
||||||
|
https://nuxtjs.org/guide/assets#webpacked |
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.** |
@ -0,0 +1,4 @@ |
|||||||
|
:root { |
||||||
|
--primary: #00b786; |
||||||
|
--secondary: #8ceaf6; |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
@import './color.css'; |
||||||
|
html { |
||||||
|
font-size: 16px; |
||||||
|
word-spacing: 1px; |
||||||
|
-ms-text-size-adjust: 100%; |
||||||
|
-webkit-text-size-adjust: 100%; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
body { |
||||||
|
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, Vazirmatn, sans-serif; |
||||||
|
} |
||||||
|
*, *:before, *:after { |
||||||
|
box-sizing: border-box; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
.btn, .pointer { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
.primary { |
||||||
|
color: var(--primary); |
||||||
|
} |
||||||
|
.secondary { |
||||||
|
color: var(--secondary); |
||||||
|
} |
||||||
|
.btn-primary { |
||||||
|
background-color: var(--primary); |
||||||
|
color: #fff; |
||||||
|
} |
||||||
|
.btn-secondary { |
||||||
|
background-color: var(--secondary); |
||||||
|
color: #000; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Apply Vazirmatn for rtl |
||||||
|
*/ |
||||||
|
|
||||||
|
.rtl .v-application *:not(.material-icons) { |
||||||
|
font-family: Vazirmatn !important; |
||||||
|
} |
||||||
|
|
||||||
|
.rtl .v-application .ml-n1 { |
||||||
|
margin-left: 0px !important; |
||||||
|
} |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 982 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 241 KiB |
After Width: | Height: | Size: 188 KiB |
@ -0,0 +1,66 @@ |
|||||||
|
html, |
||||||
|
body, |
||||||
|
#__nuxt, |
||||||
|
.ant-layout, |
||||||
|
main { |
||||||
|
@apply m-0 h-full w-full bg-white dark:(bg-black text-white); |
||||||
|
} |
||||||
|
|
||||||
|
main { |
||||||
|
@apply flex-0 w-full relative scrollbar-thin-primary; |
||||||
|
overflow-x: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
nav, |
||||||
|
nav .v-list { |
||||||
|
@apply dark:(!bg-gray-900 text-white) |
||||||
|
} |
||||||
|
|
||||||
|
.v-divider { |
||||||
|
@apply dark:bg-white |
||||||
|
} |
||||||
|
|
||||||
|
.page-enter-active, |
||||||
|
.page-leave-active, |
||||||
|
.layout-enter-active, |
||||||
|
.layout-leave-active { |
||||||
|
@apply transition-opacity duration-300 ease-in-out; |
||||||
|
} |
||||||
|
|
||||||
|
.page-enter, |
||||||
|
.page-leave-active, |
||||||
|
.layout-enter, |
||||||
|
.layout-leave-active { |
||||||
|
@apply opacity-0; |
||||||
|
} |
||||||
|
|
||||||
|
.slide-enter-active, |
||||||
|
.slide-leave-active { |
||||||
|
@apply transition-all duration-200 ease-in-out; |
||||||
|
transform: translate(100%, 0); |
||||||
|
} |
||||||
|
|
||||||
|
.slide-enter, |
||||||
|
.slide-leave-active { |
||||||
|
transform: translate(-100%, 0); |
||||||
|
} |
||||||
|
|
||||||
|
a { |
||||||
|
@apply prose text-primary underline hover:opacity-75 dark:(text-secondary) hover:(opacity-75); |
||||||
|
} |
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, p, label, button, textarea, select { |
||||||
|
@apply dark:(!text-white); |
||||||
|
} |
||||||
|
|
||||||
|
.nc-icon { |
||||||
|
@apply color-transition; |
||||||
|
} |
||||||
|
|
||||||
|
:root { |
||||||
|
--header-height: 64px; |
||||||
|
} |
||||||
|
|
||||||
|
html { |
||||||
|
overflow-y: auto !important; |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
::-webkit-scrollbar { |
||||||
|
width: .7em; |
||||||
|
height: .7em |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-button { |
||||||
|
background: #77777722 |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-track-piece { |
||||||
|
background: #66666622 |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb { |
||||||
|
background: #888; |
||||||
|
border-radius: .7em; |
||||||
|
border: .15em solid #00000000; |
||||||
|
background-clip: padding-box; |
||||||
|
} |
@ -0,0 +1,230 @@ |
|||||||
|
/* roboto-100 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 100; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-100italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 100; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-300italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 300; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-300 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 300; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 400; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-500 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 500; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-500italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 500; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 700; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-700italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 700; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-900 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 900; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
/* roboto-900italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */ |
||||||
|
@font-face { |
||||||
|
font-family: 'Roboto'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 900; |
||||||
|
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.eot'); /* IE9 Compat Modes */ |
||||||
|
src: local(''), |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.woff2') format('woff2'), /* Super Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.woff') format('woff'), /* Modern Browsers */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.ttf') format('truetype'), /* Safari, Android, iOS */ |
||||||
|
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900italic.svg#Roboto') format('svg'); /* Legacy iOS */ |
||||||
|
} |
||||||
|
|
||||||
|
/* Vazirmatn */ |
||||||
|
/* https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v32.102/Vazirmatn-font-face.css */ |
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Thin.woff2') format('woff2'); |
||||||
|
font-weight: 100; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-ExtraLight.woff2') format('woff2'); |
||||||
|
font-weight: 200; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Light.woff2') format('woff2'); |
||||||
|
font-weight: 300; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Regular.woff2') format('woff2'); |
||||||
|
font-weight: 400; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Medium.woff2') format('woff2'); |
||||||
|
font-weight: 500; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-SemiBold.woff2') format('woff2'); |
||||||
|
font-weight: 600; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Bold.woff2') format('woff2'); |
||||||
|
font-weight: 700; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-ExtraBold.woff2') format('woff2'); |
||||||
|
font-weight: 800; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: Vazirmatn; |
||||||
|
src: url('./vazirmatn/Vazirmatn-Black.woff2') format('woff2'); |
||||||
|
font-weight: 900; |
||||||
|
font-style: normal; |
||||||
|
font-display: swap; |
||||||
|
} |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 48 KiB |
@ -0,0 +1,53 @@ |
|||||||
|
// generated by unplugin-vue-components
|
||||||
|
// We suggest you to commit this file into source control
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import '@vue/runtime-core' |
||||||
|
|
||||||
|
export {} |
||||||
|
|
||||||
|
declare module '@vue/runtime-core' { |
||||||
|
export interface GlobalComponents { |
||||||
|
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete'] |
||||||
|
AButton: typeof import('ant-design-vue/es')['Button'] |
||||||
|
ACard: typeof import('ant-design-vue/es')['Card'] |
||||||
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] |
||||||
|
ACol: typeof import('ant-design-vue/es')['Col'] |
||||||
|
ACollapse: typeof import('ant-design-vue/es')['Collapse'] |
||||||
|
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] |
||||||
|
ADivider: typeof import('ant-design-vue/es')['Divider'] |
||||||
|
ADropdown: typeof import('ant-design-vue/es')['Dropdown'] |
||||||
|
AForm: typeof import('ant-design-vue/es')['Form'] |
||||||
|
AFormItem: typeof import('ant-design-vue/es')['FormItem'] |
||||||
|
AInput: typeof import('ant-design-vue/es')['Input'] |
||||||
|
AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] |
||||||
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] |
||||||
|
AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] |
||||||
|
ALayout: typeof import('ant-design-vue/es')['Layout'] |
||||||
|
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] |
||||||
|
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] |
||||||
|
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] |
||||||
|
AMenu: typeof import('ant-design-vue/es')['Menu'] |
||||||
|
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] |
||||||
|
AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] |
||||||
|
AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup'] |
||||||
|
AModal: typeof import('ant-design-vue/es')['Modal'] |
||||||
|
APagination: typeof import('ant-design-vue/es')['Pagination'] |
||||||
|
ARow: typeof import('ant-design-vue/es')['Row'] |
||||||
|
ASelect: typeof import('ant-design-vue/es')['Select'] |
||||||
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] |
||||||
|
ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] |
||||||
|
ASpace: typeof import('ant-design-vue/es')['Space'] |
||||||
|
ASpin: typeof import('ant-design-vue/es')['Spin'] |
||||||
|
ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] |
||||||
|
ATable: typeof import('ant-design-vue/es')['Table'] |
||||||
|
ATableColumn: typeof import('ant-design-vue/es')['TableColumn'] |
||||||
|
ATableColumnGroup: typeof import('ant-design-vue/es')['TableColumnGroup'] |
||||||
|
ATabPane: typeof import('ant-design-vue/es')['TabPane'] |
||||||
|
ATabs: typeof import('ant-design-vue/es')['Tabs'] |
||||||
|
ATag: typeof import('ant-design-vue/es')['Tag'] |
||||||
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] |
||||||
|
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] |
||||||
|
RouterLink: typeof import('vue-router')['RouterLink'] |
||||||
|
RouterView: typeof import('vue-router')['RouterView'] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,338 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useToast } from 'vue-toastification' |
||||||
|
import { inject, ref, useProject, watchEffect } from '#imports' |
||||||
|
import { useNuxtApp } from '#app' |
||||||
|
import { ColumnInj, MetaInj } from '~/context' |
||||||
|
import { NOCO } from '~/lib/constants' |
||||||
|
import { isImage } from '~/utils/fileUtils' |
||||||
|
import MaterialPlusIcon from '~icons/mdi/plus' |
||||||
|
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: string | any[] | null |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const isPublicForm = inject<boolean>('isPublicForm', false) |
||||||
|
const isForm = inject<boolean>('isForm', false) |
||||||
|
const meta = inject(MetaInj) |
||||||
|
const column = inject(ColumnInj) |
||||||
|
const editEnabled = inject<boolean>('editEnabled', false) |
||||||
|
|
||||||
|
const localFilesState = reactive([]) |
||||||
|
const attachments = ref([]) |
||||||
|
const uploading = ref(false) |
||||||
|
const fileInput = ref<HTMLInputElement>() |
||||||
|
|
||||||
|
const { $api } = useNuxtApp() |
||||||
|
const { project } = useProject() |
||||||
|
const toast = useToast() |
||||||
|
|
||||||
|
watchEffect(() => { |
||||||
|
if (modelValue) { |
||||||
|
attachments.value = ((typeof modelValue === 'string' ? JSON.parse(modelValue) : modelValue) || []).filter(Boolean) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const selectImage = (file: any, i) => { |
||||||
|
// todo: implement |
||||||
|
} |
||||||
|
|
||||||
|
const openUrl = (url: string, target = '_blank') => { |
||||||
|
window.open(url, target) |
||||||
|
} |
||||||
|
|
||||||
|
const addFile = () => { |
||||||
|
fileInput.value?.click() |
||||||
|
} |
||||||
|
|
||||||
|
const onFileSelection = async (e) => { |
||||||
|
// if (this.isPublicGrid) { |
||||||
|
// return |
||||||
|
// } |
||||||
|
// if (!this.$refs.file.files || !this.$refs.file.files.length) { |
||||||
|
// return |
||||||
|
// } |
||||||
|
|
||||||
|
// if (this.isPublicForm) { |
||||||
|
// this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => { |
||||||
|
// const res = { file, title: file.name } |
||||||
|
// if (isImage(file.name, file.mimetype)) { |
||||||
|
// const reader = new FileReader() |
||||||
|
// reader.onload = (e) => { |
||||||
|
// this.$set(res, 'data', e.target.result) |
||||||
|
// } |
||||||
|
// reader.readAsDataURL(file) |
||||||
|
// } |
||||||
|
// return res |
||||||
|
// })) |
||||||
|
// |
||||||
|
// this.$emit('input', this.localFilesState.map(f => f.file)) |
||||||
|
// return |
||||||
|
// } |
||||||
|
|
||||||
|
// todo : move to com |
||||||
|
uploading.value = true |
||||||
|
const newAttachments = [] |
||||||
|
for (const file of fileInput.value?.files ?? []) { |
||||||
|
try { |
||||||
|
const data = await $api.storage.upload( |
||||||
|
{ |
||||||
|
path: [NOCO, project.value.title, meta?.value?.title, column?.title].join('/'), |
||||||
|
}, |
||||||
|
{ |
||||||
|
files: file, |
||||||
|
json: '{}', |
||||||
|
}, |
||||||
|
) |
||||||
|
newAttachments.push(...data) |
||||||
|
} catch (e: any) { |
||||||
|
toast.error(e.message || 'Some internal error occurred') |
||||||
|
uploading.value = false |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
uploading.value = false |
||||||
|
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments])) |
||||||
|
|
||||||
|
// this.$emit('input', JSON.stringify(this.localState)) |
||||||
|
// this.$emit('update') |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="main h-100"> |
||||||
|
<div class="d-flex align-center img-container"> |
||||||
|
<div class="d-flex no-overflow"> |
||||||
|
<div |
||||||
|
v-for="(item, i) in isPublicForm ? localFilesState : attachments" |
||||||
|
:key="item.url || item.title" |
||||||
|
class="thumbnail align-center justify-center d-flex" |
||||||
|
> |
||||||
|
<!-- <v-tooltip bottom> --> |
||||||
|
<!-- <template #activator="{ on }"> --> |
||||||
|
<!-- <v-img |
||||||
|
v-if="isImage(item.title, item.mimetype)" |
||||||
|
lazy-src="https://via.placeholder.com/60.png?text=Loading..." |
||||||
|
alt="#" |
||||||
|
max-height="99px" |
||||||
|
contain |
||||||
|
:src="item.url || item.data" |
||||||
|
v-on="on" |
||||||
|
@click="selectImage(item.url || item.data, i)" |
||||||
|
> --> |
||||||
|
<img |
||||||
|
v-if="isImage(item.title, item.mimetype)" |
||||||
|
alt="#" |
||||||
|
style="max-height: 30px; max-width: 30px" |
||||||
|
:src="item.url || item.data" |
||||||
|
@click="selectImage(item.url || item.data, i)" |
||||||
|
/> |
||||||
|
<!-- <template #placeholder> --> |
||||||
|
<!-- <v-skeleton-loader type="image" :height="active ? 33 : 22" :width="active ? 33 : 22" /> --> |
||||||
|
<!-- </template> --> |
||||||
|
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> |
||||||
|
{{ item.icon }} |
||||||
|
</v-icon> |
||||||
|
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')"> mdi-file </v-icon> |
||||||
|
<!-- </template> --> |
||||||
|
<!-- <span>{{ item.title }}</span> --> |
||||||
|
<!-- </v-tooltip> --> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- todo: hide or toggle based on ancestor --> |
||||||
|
<div class="add d-flex align-center justify-center px-1 nc-attachment-add" @click="addFile"> |
||||||
|
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon> |
||||||
|
<!-- <v-btn v-else-if="isForm" outlined x-small color="" text class="nc-attachment-add-btn"> |
||||||
|
<v-icon x-small color="" icon="MaterialPlusIcon"> mdi-plus </v-icon> |
||||||
|
Attachment |
||||||
|
</v-btn> |
||||||
|
<v-icon small color="primary nc-attachment-add-icon"> |
||||||
|
mdi-plus |
||||||
|
</v-icon> --> |
||||||
|
<MaterialPlusIcon /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<v-spacer /> |
||||||
|
|
||||||
|
<MaterialArrowExpandIcon @click.stop="dialog = true" /> |
||||||
|
<!-- <v-icon class="expand-icon mr-1" x-small color="primary" @click.stop="dialog = true"> mdi-arrow-expand </v-icon> --> |
||||||
|
</div> |
||||||
|
|
||||||
|
<input ref="fileInput" type="file" multiple class="d-none" @change="onFileSelection" /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
.thumbnail { |
||||||
|
height: 30px; |
||||||
|
width: 30px; |
||||||
|
margin: 2px; |
||||||
|
border-radius: 4px; |
||||||
|
|
||||||
|
img { |
||||||
|
max-height: 33px; |
||||||
|
max-width: 33px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.expand-icon { |
||||||
|
margin-left: 8px; |
||||||
|
border-radius: 2px; |
||||||
|
transition: 0.3s background-color; |
||||||
|
} |
||||||
|
|
||||||
|
.expand-icon:hover { |
||||||
|
background-color: var(--v-primary-lighten4); |
||||||
|
} |
||||||
|
|
||||||
|
/*.img-container { |
||||||
|
margin: 0 -2px; |
||||||
|
} |
||||||
|
|
||||||
|
.no-overflow { |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.add { |
||||||
|
transition: 0.2s background-color; |
||||||
|
!*background-color: #666666ee;*! |
||||||
|
border-radius: 4px; |
||||||
|
height: 33px; |
||||||
|
margin: 5px 2px; |
||||||
|
} |
||||||
|
|
||||||
|
.add:hover { |
||||||
|
!*background-color: #66666699;*! |
||||||
|
} |
||||||
|
|
||||||
|
.thumbnail { |
||||||
|
height: 99px; |
||||||
|
width: 99px; |
||||||
|
margin: 2px; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.thumbnail img { |
||||||
|
!*max-height: 33px;*! |
||||||
|
max-width: 99px; |
||||||
|
} |
||||||
|
|
||||||
|
.main { |
||||||
|
min-height: 20px; |
||||||
|
position: relative; |
||||||
|
height: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.expand-icon { |
||||||
|
margin-left: 8px; |
||||||
|
border-radius: 2px; |
||||||
|
!*opacity: 0;*! |
||||||
|
transition: 0.3s background-color; |
||||||
|
} |
||||||
|
|
||||||
|
.expand-icon:hover { |
||||||
|
!*opacity: 1;*! |
||||||
|
background-color: var(--v-primary-lighten4); |
||||||
|
} |
||||||
|
|
||||||
|
.modal-thumbnail img { |
||||||
|
height: 50px; |
||||||
|
max-width: 100%; |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-thumbnail { |
||||||
|
position: relative; |
||||||
|
margin: 10px 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.remove-icon { |
||||||
|
position: absolute; |
||||||
|
top: 5px; |
||||||
|
right: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-thumbnail-card { |
||||||
|
.download-icon { |
||||||
|
position: absolute; |
||||||
|
bottom: 5px; |
||||||
|
right: 5px; |
||||||
|
opacity: 0; |
||||||
|
transition: 0.4s opacity; |
||||||
|
} |
||||||
|
|
||||||
|
&:hover .download-icon { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.image-overlay-container { |
||||||
|
max-height: 100vh; |
||||||
|
overflow-y: auto; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.image-overlay-container .close-icon { |
||||||
|
position: fixed; |
||||||
|
top: 15px; |
||||||
|
right: 15px; |
||||||
|
} |
||||||
|
|
||||||
|
.overlay-thumbnail { |
||||||
|
transition: 0.4s transform, 0.4s opacity; |
||||||
|
opacity: 0.5; |
||||||
|
} |
||||||
|
|
||||||
|
.overlay-thumbnail.active { |
||||||
|
transform: scale(1.4); |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.overlay-thumbnail:hover { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-title { |
||||||
|
text-overflow: ellipsis; |
||||||
|
white-space: nowrap; |
||||||
|
width: 100%; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-thumbnail-card { |
||||||
|
transition: 0.4s transform; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-thumbnail-card:hover { |
||||||
|
transform: scale(1.05); |
||||||
|
} |
||||||
|
|
||||||
|
.drop-overlay { |
||||||
|
z-index: 5; |
||||||
|
position: absolute; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
top: 0; |
||||||
|
bottom: 5px; |
||||||
|
background: #aaaaaa44; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
|
||||||
|
.expand-icon { |
||||||
|
opacity: 0; |
||||||
|
transition: 0.4s opacity; |
||||||
|
} |
||||||
|
|
||||||
|
.main:hover .expand-icon { |
||||||
|
opacity: 1; |
||||||
|
}*/ |
||||||
|
</style> |
@ -0,0 +1,91 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { computed, inject } from '#imports' |
||||||
|
import { ColumnInj, IsFormInj } from '~/context' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue?: boolean | undefined | number |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue: value } = defineProps<Props>() |
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
const column = inject(ColumnInj) |
||||||
|
const isForm = inject(IsFormInj) |
||||||
|
|
||||||
|
const checkboxMeta = computed(() => { |
||||||
|
return { |
||||||
|
icon: { |
||||||
|
checked: 'mdi-check-circle-outline', |
||||||
|
unchecked: 'mdi-checkbox-blank-circle-outline', |
||||||
|
}, |
||||||
|
color: 'primary', |
||||||
|
...(column?.meta || {}), |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const localState = computed({ |
||||||
|
get: () => value, |
||||||
|
set: (val) => emit('update:modelValue', val), |
||||||
|
}) |
||||||
|
|
||||||
|
// const checkedIcon = computed(() => { |
||||||
|
// return defineAsyncComponent( ()=>import('~icons/material-symbols/'+checkboxMeta?.value?.icon?.checked)) |
||||||
|
// }); |
||||||
|
// const uncheckedIcon = computed(() => { |
||||||
|
// return defineAsyncComponent(()=>import('~icons/material-symbols/'+checkboxMeta?.value?.icon?.unchecked)) |
||||||
|
// }); |
||||||
|
|
||||||
|
/* export default { |
||||||
|
name: 'BooleanCell', |
||||||
|
props: { |
||||||
|
column: Object, |
||||||
|
value: [String, Number, Boolean], |
||||||
|
isForm: Boolean, |
||||||
|
readOnly: Boolean, |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
checkedIcon() { |
||||||
|
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.checked) || 'mdi-check-bold' |
||||||
|
}, |
||||||
|
uncheckedIcon() { |
||||||
|
return (this.checkboxMeta && this.checkboxMeta.icon && this.checkboxMeta.icon.unchecked) || 'mdi-crop-square' |
||||||
|
}, |
||||||
|
localState: { |
||||||
|
get() { |
||||||
|
return this.value |
||||||
|
}, |
||||||
|
set(val) { |
||||||
|
this.$emit('input', val) |
||||||
|
}, |
||||||
|
}, |
||||||
|
parentListeners() { |
||||||
|
const $listeners = {} |
||||||
|
return $listeners |
||||||
|
}, |
||||||
|
checkboxMeta() { |
||||||
|
return { |
||||||
|
icon: { |
||||||
|
checked: 'mdi-check-circle-outline', |
||||||
|
unchecked: 'mdi-checkbox-blank-circle-outline', |
||||||
|
}, |
||||||
|
color: 'primary', |
||||||
|
...(this.column && this.column.meta ? this.column.meta : {}), |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggle() { |
||||||
|
this.localState = !this.localState |
||||||
|
}, |
||||||
|
}, |
||||||
|
} */ |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="d-flex align-center" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !localState }"> |
||||||
|
<!-- <span :is="localState ? checkedIcon : uncheckedIcon" small :color="checkboxMeta.color" @click="toggle"> --> |
||||||
|
<!-- {{ localState ? checkedIcon : uncheckedIcon }} --> |
||||||
|
<!-- </span> --> |
||||||
|
|
||||||
|
<input v-model="localState" type="checkbox" /> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,37 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: 'CurrencyCell', |
||||||
|
props: { |
||||||
|
column: Object, |
||||||
|
value: [String, Number], |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
currency() { |
||||||
|
try { |
||||||
|
return isNaN(this.value) |
||||||
|
? this.value |
||||||
|
: new Intl.NumberFormat(this.currencyMeta.currency_locale || 'en-US', { |
||||||
|
style: 'currency', |
||||||
|
currency: this.currencyMeta.currency_code || 'USD', |
||||||
|
}).format(this.value) |
||||||
|
} catch (e) { |
||||||
|
return this.value |
||||||
|
} |
||||||
|
}, |
||||||
|
currencyMeta() { |
||||||
|
return { |
||||||
|
currency_locale: 'en-US', |
||||||
|
currency_code: 'USD', |
||||||
|
...(this.column && this.column.meta ? this.column.meta : {}), |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a v-if="value">{{ currency }}</a> |
||||||
|
<span v-else /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,91 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import dayjs from 'dayjs' |
||||||
|
import { computed } from '#imports' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: string |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const localState = computed({ |
||||||
|
get() { |
||||||
|
if (!modelValue || !dayjs(modelValue).isValid()) { |
||||||
|
return undefined |
||||||
|
} |
||||||
|
|
||||||
|
return (/^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue)).format('YYYY-MM-DD') |
||||||
|
}, |
||||||
|
set(val?: string) { |
||||||
|
if (dayjs(val).isValid()) { |
||||||
|
emit('update:modelValue', val && dayjs(val).format('YYYY-MM-DD')) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
/* |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DatePickerCell', |
||||||
|
props: { |
||||||
|
value: [String, Date], |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
localState: { |
||||||
|
get() { |
||||||
|
if (!this.value || !dayjs(this.value).isValid()) { |
||||||
|
return undefined |
||||||
|
} |
||||||
|
|
||||||
|
return (/^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value)).format('YYYY-MM-DD') |
||||||
|
}, |
||||||
|
set(val) { |
||||||
|
if (dayjs(val).isValid()) { |
||||||
|
this.$emit('input', val && dayjs(val).format('YYYY-MM-DD')) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
date() { |
||||||
|
if (!this.value || this.localState) { |
||||||
|
return this.localState |
||||||
|
} |
||||||
|
return 'Invalid Date' |
||||||
|
}, |
||||||
|
parentListeners() { |
||||||
|
const $listeners = {} |
||||||
|
|
||||||
|
if (this.$listeners.blur) { |
||||||
|
$listeners.blur = this.$listeners.blur |
||||||
|
} |
||||||
|
if (this.$listeners.focus) { |
||||||
|
$listeners.focus = this.$listeners.focus |
||||||
|
} |
||||||
|
|
||||||
|
return $listeners |
||||||
|
}, |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
if (this.$el && this.$el.$el) { |
||||||
|
this.$el.$el.focus() |
||||||
|
} |
||||||
|
}, |
||||||
|
} */ |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<!-- <v-menu> --> |
||||||
|
<!-- <template #activator="{ on }"> --> |
||||||
|
<input v-model="localState" type="date" class="value" /> |
||||||
|
<!-- </template> --> |
||||||
|
<!-- <v-date-picker v-model="localState" flat @click.native.stop v-on="parentListeners" /> --> |
||||||
|
<!-- </v-menu> --> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.value { |
||||||
|
width: 100%; |
||||||
|
min-height: 20px; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,146 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import dayjs from 'dayjs' |
||||||
|
import { computed, ref, useProject } from '#imports' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue?: string |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const { isMysql } = useProject() |
||||||
|
const showMessage = ref(false) |
||||||
|
|
||||||
|
const localState = computed({ |
||||||
|
get() { |
||||||
|
if (!modelValue) { |
||||||
|
return modelValue |
||||||
|
} |
||||||
|
const d = /^\d+$/.test(modelValue) ? dayjs(+modelValue) : dayjs(modelValue) |
||||||
|
if (d.isValid()) { |
||||||
|
showMessage.value = false |
||||||
|
return d.format('YYYY-MM-DD HH:mm') |
||||||
|
} else { |
||||||
|
showMessage.value = true |
||||||
|
} |
||||||
|
}, |
||||||
|
set(value?: string) { |
||||||
|
if (isMysql) { |
||||||
|
emit('update:modelValue', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss')) |
||||||
|
} else { |
||||||
|
emit('update:modelValue', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ')) |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
/* import dayjs from 'dayjs' |
||||||
|
import utc from 'dayjs/plugin/utc' |
||||||
|
|
||||||
|
dayjs.extend(utc) |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'DateTimePickerCell', |
||||||
|
props: { |
||||||
|
value: [String, Date, Number], |
||||||
|
ignoreFocus: Boolean, |
||||||
|
}, |
||||||
|
data: () => ({ |
||||||
|
showMessage: false, |
||||||
|
}), |
||||||
|
computed: { |
||||||
|
isMysql() { |
||||||
|
return ['mysql', 'mysql2'].indexOf(this.$store.getters['project/GtrClientType']) |
||||||
|
}, |
||||||
|
localState: { |
||||||
|
get() { |
||||||
|
if (!this.value) { |
||||||
|
return this.value |
||||||
|
} |
||||||
|
const d = /^\d+$/.test(this.value) ? dayjs(+this.value) : dayjs(this.value) |
||||||
|
if (d.isValid()) { |
||||||
|
this.showMessage = false |
||||||
|
return d.format('YYYY-MM-DD HH:mm') |
||||||
|
} else { |
||||||
|
this.showMessage = true |
||||||
|
} |
||||||
|
}, |
||||||
|
set(value) { |
||||||
|
if (this.isMysql) { |
||||||
|
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ss')) |
||||||
|
} else { |
||||||
|
this.$emit('input', value && dayjs(value).format('YYYY-MM-DD HH:mm:ssZ')) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
parentListeners() { |
||||||
|
const $listeners = {} |
||||||
|
|
||||||
|
if (this.$listeners.blur) { |
||||||
|
// $listeners.blur = this.$listeners.blur |
||||||
|
} |
||||||
|
if (this.$listeners.focus) { |
||||||
|
$listeners.focus = this.$listeners.focus |
||||||
|
} |
||||||
|
|
||||||
|
return $listeners |
||||||
|
}, |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// listen dialog click:outside event and save on close |
||||||
|
if (this.$refs.picker && this.$refs.picker.$children && this.$refs.picker.$children[0]) { |
||||||
|
this.$refs.picker.$children[0].$on('click:outside', () => { |
||||||
|
this.$refs.picker.okHandler() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.ignoreFocus) { |
||||||
|
this.$refs.picker.display = true |
||||||
|
} |
||||||
|
}, |
||||||
|
} */ |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input v-model="localState" type="datetime-local" /> |
||||||
|
<!-- <div> --> |
||||||
|
<!-- <div v-show="!showMessage"> --> |
||||||
|
<!-- <v-datetime-picker --> |
||||||
|
<!-- ref="picker" --> |
||||||
|
<!-- v-model="localState" --> |
||||||
|
<!-- class="caption xc-date-time-picker" --> |
||||||
|
<!-- :text-field-props="{ --> |
||||||
|
<!-- class: 'caption mt-0 pt-0', --> |
||||||
|
<!-- flat: true, --> |
||||||
|
<!-- solo: true, --> |
||||||
|
<!-- dense: true, --> |
||||||
|
<!-- hideDetails: true, --> |
||||||
|
<!-- }" --> |
||||||
|
<!-- :time-picker-props="{ --> |
||||||
|
<!-- format: '24hr', --> |
||||||
|
<!-- }" --> |
||||||
|
<!-- v-on="parentListeners" --> |
||||||
|
<!-- /> --> |
||||||
|
<!-- </div> --> |
||||||
|
<!-- <div v-show="showMessage" class="edit-warning" @dblclick="$refs.picker.display = true"> --> |
||||||
|
<!-- <!– TODO: i18n –> --> |
||||||
|
<!-- ERR: Couldn't parse {{ value }} --> |
||||||
|
<!-- </div> --> |
||||||
|
<!-- </div> --> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
/*:deep(.v-input),*/ |
||||||
|
/*:deep(.v-text-field) {*/ |
||||||
|
/* margin-top: 0 !important;*/ |
||||||
|
/* padding-top: 0 !important;*/ |
||||||
|
/* font-size: inherit !important;*/ |
||||||
|
/*}*/ |
||||||
|
|
||||||
|
/*.edit-warning {*/ |
||||||
|
/* padding: 10px;*/ |
||||||
|
/* text-align: left;*/ |
||||||
|
/* color: #e65100;*/ |
||||||
|
/*}*/ |
||||||
|
</style> |
@ -0,0 +1,110 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { computed, inject, ref } from '#imports' |
||||||
|
import { ColumnInj } from '~/context' |
||||||
|
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils/durationHelper' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: number | string |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const column = inject(ColumnInj) |
||||||
|
|
||||||
|
const showWarningMessage = ref(false) |
||||||
|
const durationInMS = ref(0) |
||||||
|
const isEdited = ref(false) |
||||||
|
const durationType = ref(column?.meta?.duration || 0) |
||||||
|
|
||||||
|
const durationPlaceholder = computed(() => durationOptions[durationType.value].title) |
||||||
|
const localState = computed({ |
||||||
|
get: () => convertMS2Duration(modelValue, durationType.value), |
||||||
|
set: (val) => { |
||||||
|
isEdited.value = true |
||||||
|
const res = convertDurationToSeconds(val, durationType.value) |
||||||
|
if (res._isValid) { |
||||||
|
durationInMS.value = res._sec |
||||||
|
} |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
const checkDurationFormat = (evt: KeyboardEvent) => { |
||||||
|
evt = evt || window.event |
||||||
|
const charCode = evt.which ? evt.which : evt.keyCode |
||||||
|
// ref: http://www.columbia.edu/kermit/ascii.html |
||||||
|
const PRINTABLE_CTL_RANGE = charCode > 31 |
||||||
|
const NON_DIGIT = charCode < 48 || charCode > 57 |
||||||
|
const NON_COLON = charCode !== 58 |
||||||
|
const NON_PERIOD = charCode !== 46 |
||||||
|
if (PRINTABLE_CTL_RANGE && NON_DIGIT && NON_COLON && NON_PERIOD) { |
||||||
|
showWarningMessage.value = true |
||||||
|
evt.preventDefault() |
||||||
|
} else { |
||||||
|
showWarningMessage.value = false |
||||||
|
// only allow digits, '.' and ':' (without quotes) |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const submitDuration = () => { |
||||||
|
if (isEdited.value) { |
||||||
|
emit('update:modelValue', durationInMS.value) |
||||||
|
} |
||||||
|
isEdited.value = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="duration-cell-wrapper"> |
||||||
|
<input |
||||||
|
ref="durationInput" |
||||||
|
v-model="localState" |
||||||
|
:placeholder="durationPlaceholder" |
||||||
|
@blur="submitDuration" |
||||||
|
@keypress="checkDurationFormat($event)" |
||||||
|
@keydown.enter="submitDuration" |
||||||
|
/> |
||||||
|
<div v-if="showWarningMessage" class="duration-warning"> |
||||||
|
<!-- TODO: i18n --> |
||||||
|
Please enter a number |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.duration-cell-wrapper { |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.duration-warning { |
||||||
|
text-align: left; |
||||||
|
margin-top: 10px; |
||||||
|
color: #e65100; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<!-- |
||||||
|
/** |
||||||
|
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
||||||
|
* |
||||||
|
* @author Wing-Kam Wong <wingkwong.code@gmail.com> |
||||||
|
* |
||||||
|
* @license GNU AGPL version 3 or any later version |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as |
||||||
|
* published by the Free Software Foundation, either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||||
|
* |
||||||
|
*/ |
||||||
|
--> |
@ -0,0 +1,24 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { computed } from '#imports' |
||||||
|
|
||||||
|
import { isEmail } from '~/utils/validation' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: string |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue } = defineProps<Props>() |
||||||
|
|
||||||
|
const validEmail = computed(() => isEmail(modelValue)) |
||||||
|
</script> |
||||||
|
|
||||||
|
<script lang="ts"> |
||||||
|
export default { |
||||||
|
name: 'EmailCell', |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a v-if="validEmail" :href="`mailto:${modelValue}`" target="_blank">{{ modelValue }}</a> |
||||||
|
<span v-else>{{ modelValue }}</span> |
||||||
|
</template> |
@ -0,0 +1,38 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import { computed, inject, onMounted, ref } from '#imports' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: number |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue: value } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const editEnabled = inject<boolean>('editEnabled') |
||||||
|
|
||||||
|
const root = ref<HTMLInputElement>() |
||||||
|
|
||||||
|
const localState = computed({ |
||||||
|
get: () => value, |
||||||
|
set: (val) => emit('update:modelValue', val), |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
root.value?.focus() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input v-if="editEnabled" ref="root" v-model="localState" type="number" /> |
||||||
|
<span v-else>{{ localState }}</span> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
input { |
||||||
|
outline: none; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
color: var(--v-textColor-base); |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,36 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
interface Props { |
||||||
|
modelValue: number |
||||||
|
} |
||||||
|
|
||||||
|
const { modelValue: value } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const editEnabled = inject<boolean>('editEnabled') |
||||||
|
|
||||||
|
const root = ref<HTMLInputElement>() |
||||||
|
|
||||||
|
const localState = computed({ |
||||||
|
get: () => value, |
||||||
|
set: (val) => emit('update:modelValue', val), |
||||||
|
}) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
root.value?.focus() |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<input v-if="editEnabled" ref="root" v-model="localState" type="number" /> |
||||||
|
<span v-else>{{ localState }}</span> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
input { |
||||||
|
outline: none; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
color: var(--v-textColor-base); |
||||||
|
} |
||||||
|
</style> |