@ -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> |
||||
<!-- <NuxtLayout>--> |
||||
<!-- <NuxtPage />--> |
||||
<!-- </NuxtLayout>--> |
||||
<a-layout> |
||||
<a-layout-header class="flex !bg-primary items-center text-white px-4 shadow-md"> |
||||
<MaterialSymbolsMenu |
||||
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> |
||||
|
@ -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> |