Browse Source

Merge branch 'refactor/gui-v2' into feat/gui-v2-templates

pull/2828/head
Wing-Kam Wong 2 years ago
parent
commit
72c9b3ec9c
  1. 101
      packages/nc-gui-v2/app.vue
  2. 40
      packages/nc-gui-v2/assets/style-v2.css
  3. 70
      packages/nc-gui-v2/assets/style-v2.scss
  4. 11
      packages/nc-gui-v2/components.d.ts
  5. 2
      packages/nc-gui-v2/components/cell/Duration.vue
  6. 9
      packages/nc-gui-v2/components/cell/Url.vue
  7. 23
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  8. 12
      packages/nc-gui-v2/components/general/Language.vue
  9. 14
      packages/nc-gui-v2/components/general/NocoIcon.vue
  10. 2
      packages/nc-gui-v2/components/general/Share.vue
  11. 2
      packages/nc-gui-v2/components/general/Social.vue
  12. 6
      packages/nc-gui-v2/components/general/Sponsors.vue
  13. 10
      packages/nc-gui-v2/layouts/default.vue
  14. 7
      packages/nc-gui-v2/nuxt.config.ts
  15. 19
      packages/nc-gui-v2/package-lock.json
  16. 1
      packages/nc-gui-v2/package.json
  17. 128
      packages/nc-gui-v2/pages/forgot-password.vue
  18. 13
      packages/nc-gui-v2/pages/index.vue
  19. 150
      packages/nc-gui-v2/pages/index/index.vue
  20. 42
      packages/nc-gui-v2/pages/index/index/index.vue
  21. 18
      packages/nc-gui-v2/pages/index/index/list.vue
  22. 42
      packages/nc-gui-v2/pages/index/user/index.vue
  23. 140
      packages/nc-gui-v2/pages/index/user/index/index.vue
  24. 2
      packages/nc-gui-v2/pages/nc/[projectId].vue
  25. 108
      packages/nc-gui-v2/pages/projects/index.vue
  26. 33
      packages/nc-gui-v2/pages/signin.vue
  27. 17
      packages/nc-gui-v2/pages/signup.vue
  28. 8
      packages/nc-gui-v2/windi.config.ts

101
packages/nc-gui-v2/app.vue

@ -12,20 +12,31 @@ const signOut = () => {
$state.signOut() $state.signOut()
navigateTo('/signin') navigateTo('/signin')
} }
const toggleSidebar = useToggle($state.sidebarOpen)
const sidebarOpen = computed({
get: () => !($state.sidebarOpen.value ?? true),
set: (val) => toggleSidebar(val),
})
</script> </script>
<template> <template>
<v-app> <a-layout>
<v-app-bar class="shadow-md bg-primary" height="48"> <a-layout-header class="flex !bg-primary items-center text-white !px-4">
<div class="flex items-center flex-1"> <MaterialSymbolsMenu
<v-toolbar-title> v-if="$state.signedIn.value"
<v-tooltip bottom> class="text-xl cursor-pointer"
{{ $t('general.home') }} @click="toggleSidebar(!$state.sidebarOpen.value)"
<span class="caption font-weight-light pointer">(version)</span> />
</v-tooltip>
<div class="flex-1" />
<span class="body-1" @click="navigateTo('/projects')">NocoDB</span>
</v-toolbar-title> <div class="ml-4 flex items-center flex-1">
<div class="flex items-center gap-2">
<img width="35" src="~/assets/img/icons/512x512-trans.png" />
<span class="prose-xl" @click="navigateTo('/')">NocoDB</span>
</div>
<!-- todo: loading is not yet supported by nuxt 3 - see https://v3.nuxtjs.org/migration/component-options#loading <!-- 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"> <span v-show="$nuxt.$loading.show" class="caption grey--text ml-3">
@ -38,54 +49,54 @@ const signOut = () => {
--> -->
</div> </div>
<div class="flex justify-end"> <div class="flex-1" />
<v-toolbar-items class="flex gap-4 nc-topright-menu">
<!-- todo: implement components
<release-info />
-->
<div class="flex justify-end gap-4">
<general-color-mode-switcher v-model="$state.darkMode.value" /> <general-color-mode-switcher v-model="$state.darkMode.value" />
<general-language class="mr-3" /> <general-language class="mr-3" />
<MaterialSymbolsMenu
v-if="$state.signedIn.value"
class="block text-xl cursor-pointer xl:(hidden)"
@click="$state.sidebarOpen.value = !$state.sidebarOpen.value"
/>
<template v-if="$state.signedIn.value"> <template v-if="$state.signedIn.value">
<v-menu class="leading-8"> <a-dropdown :trigger="['click']">
<template #activator="{ props }"> <MdiDotsVertical class="md:text-xl cursor-pointer" @click.prevent />
<MdiDotsVertical class="md:text-xl cursor-pointer" @click="props.onClick" />
</template> <template #overlay>
<v-list class="!py-0 nc-user-menu min-w-32"> <a-menu class="!py-0 nc-user-menu min-w-32 dark:(!bg-gray-800) leading-8 !rounded">
<nuxt-link <a-menu-item key="0" class="!rounded">
v-t="['c:navbar:user:email']" <nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
class="group hover:bg-gray-200 flex items-center p-2" <MdiAt class="mt-1 group-hover:text-success" />&nbsp;
to="/user/settings"
>
<MdiAt class="mt-1 transition-colors duration-150 ease-in group-hover:text-success" />&nbsp;
<span class="prose">{{ email }}</span> <span class="prose">{{ email }}</span>
</nuxt-link> </nuxt-link>
</a-menu-item>
<v-divider /> <a-menu-divider class="!m-0" />
<div <a-menu-item key="1" class="!rounded">
v-t="['a:navbar:user:sign-out']" <div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="signOut">
class="group flex flex-row cursor-pointer hover:bg-gray-200 flex items-center p-2" <MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
@click="signOut"
>
<MdiLogout class="transition-colors duration-150 ease-in group-hover:text-red-500" />&nbsp;
<span class="prose font-semibold text-gray-500">{{ $t('general.signOut') }}</span> <span class="prose font-semibold text-gray-500">{{ $t('general.signOut') }}</span>
</div> </div>
</v-list> </a-menu-item>
</v-menu> </a-menu>
</template>
</a-dropdown>
</template> </template>
</v-toolbar-items>
</div> </div>
</v-app-bar> </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" class="w-full h-full" />
</a-layout-sider>
<NuxtPage /> <NuxtPage />
</v-app> </a-layout>
</a-layout>
</template> </template>

40
packages/nc-gui-v2/assets/style-v2.css

@ -1,40 +0,0 @@
html,
body,
#__nuxt,
.v-application__wrap {
@apply m-0 h-full w-full bg-white dark:(bg-black text-white);
}
.v-main {
@apply w-full h-full;
overflow: hidden;
flex: unset !important;
}
.v-main .v-main__wrap {
@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;
}

70
packages/nc-gui-v2/assets/style-v2.scss

@ -0,0 +1,70 @@
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 {
@apply text-black dark:(text-white);
}
.v-field__field {
@apply bg-white dark:(!bg-gray-900 text-white);
input {
@apply bg-white dark:(!bg-gray-700) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;
}
}
.nc-icon {
@apply color-transition;
}
:root {
--header-height: 64px;
}

11
packages/nc-gui-v2/components.d.ts vendored

@ -7,6 +7,17 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
ADivider: typeof import('ant-design-vue/es')['Divider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
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']
AModal: typeof import('ant-design-vue/es')['Modal']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

2
packages/nc-gui-v2/components/cell/Duration.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, inject } from '#imports' import { computed, inject, ref } from '#imports'
import { ColumnInj } from '~/components' import { ColumnInj } from '~/components'
import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils/durationHelper' import { convertDurationToSeconds, convertMS2Duration, durationOptions } from '~/utils/durationHelper'

9
packages/nc-gui-v2/components/cell/Url.vue

@ -1,17 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from '#imports' import { computed, ref } from '#imports'
import { ColumnInj } from '~/components' import { ColumnInj } from '~/components'
import { isValidURL } from '~/utils/urlUtils' import { isValidURL } from '~/utils/urlUtils'
const column = inject(ColumnInj)
const editEnabled = inject<boolean>('editEnabled')
interface Props { interface Props {
modelValue: string modelValue: string
} }
const { modelValue: value } = defineProps<Props>() const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)
const editEnabled = inject<boolean>('editEnabled')
const localState = computed({ const localState = computed({
get: () => value, get: () => value,
@ -35,7 +34,7 @@ onMounted(() => {
<input ref="root" v-model="localState" /> <input ref="root" v-model="localState" />
</span> </span>
<span v-else> <span v-else>
<a class="caption py-2 text-primary underline hover:opacity-75" v-if="isValid" :href="value" target="_blank">{{ value }}</a> <a v-if="isValid" class="caption py-2 text-primary underline hover:opacity-75" :href="value" target="_blank">{{ value }}</a>
<span v-else>{{ value }}</span> <span v-else>{{ value }}</span>
</span> </span>
</template> </template>

23
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -1,23 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import useProject from '~/composables/useProject' import useProject from '~/composables/useProject'
import useTabs from '~/composables/useTabs' import useTabs from '~/composables/useTabs'
import MdiSettingIcon from '~icons/mdi/cog'
const { tables } = useProject() const { tables } = useProject()
const { addTab } = useTabs() const { addTab } = useTabs()
const settingsDlg = ref(false)
</script> </script>
<template> <template>
<div> <div class="nc-treeview-container flex flex-column">
<v-list> <a-menu class="flex-1 overflow-y-auto">
<v-list-item <a-menu-item
v-for="table in tables" v-for="table in tables"
:key="table.id" :key="table.id"
class="p-2 text-sm pointer" class="p-2 text-sm pointer"
@click="addTab({ type: 'table', title: table.title, id: table.id })" @click="addTab({ type: 'table', title: table.title, id: table.id })"
> >
{{ table.title }} {{ table.title }}
</v-list-item> </a-menu-item>
</v-list> </a-menu>
<div class="cursor-pointer nc-team-settings pa-4 flex align-center hover:bg-gray-200/20" @click="settingsDlg = true">
<MdiSettingIcon class="mr-2" />
<span> {{ $t('title.teamAndSettings') }}</span>
</div>
<a-modal v-model:visible="settingsDlg" width="max(90vw, 600px)"> Team and settings </a-modal>
</div> </div>
</template> </template>
@ -25,4 +34,8 @@ const { addTab } = useTabs()
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
.nc-treeview-container {
height: calc(100vh - var(--header-height));
}
</style> </style>

12
packages/nc-gui-v2/components/general/Language.vue

@ -38,18 +38,18 @@ onMounted(() => {
<template #activator="{ props }"> <template #activator="{ props }">
<MaterialSymbolsTranslate class="md:text-xl cursor-pointer" @click="props.onClick" /> <MaterialSymbolsTranslate class="md:text-xl cursor-pointer" @click="props.onClick" />
</template> </template>
<v-list class="min-w-50 max-h-90vh overflow-auto !py-0 scrollbar-thin-primary"> <v-list class="scrollbar min-w-50 max-h-90vh overflow-auto !py-0 dark:(!bg-gray-800 !text-white)">
<v-list-item <v-list-item
v-for="lang of languages" v-for="lang of languages"
:key="lang.value" :key="lang.value"
:class="lang === locale ? '!bg-primary/10 text-primary' : ''" :class="lang === locale ? '!bg-primary/10 text-primary dark:(!bg-gray-700 !text-secondary)' : ''"
class="!min-h-8 group" class="!min-h-8 group"
:value="lang" :value="lang"
@click="changeLanguage(lang)" @click="changeLanguage(lang)"
> >
<v-list-item-subtitle <v-list-item-subtitle
:class="lang === locale ? '!font-semibold' : ''" :class="lang === locale ? '!font-semibold' : ''"
class="capitalize md:(!leading-8) group-hover:(text-primary font-semibold)" class="capitalize md:(!leading-8) group-hover:(text-primary font-semibold) dark:(group-hover:text-secondary)"
> >
{{ Language[lang] || lang }} {{ Language[lang] || lang }}
</v-list-item-subtitle> </v-list-item-subtitle>
@ -69,3 +69,9 @@ onMounted(() => {
</v-list> </v-list>
</v-menu> </v-menu>
</template> </template>
<style scoped>
.scrollbar {
@apply scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white dark:(!scrollbar-track-gray-900);
}
</style>

14
packages/nc-gui-v2/components/general/NocoIcon.vue

@ -0,0 +1,14 @@
<script lang="ts" setup>
interface Props {
width?: number
height?: number
}
const { width = 90, height = 90 } = defineProps<Props>()
</script>
<template>
<div :style="{ left: `calc(50% - ${width / 2}px)`, top: `-${height * 0.6}px` }" class="absolute rounded-lg bg-primary">
<img :width="width" :height="height" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
</template>

2
packages/nc-gui-v2/components/general/Share.vue

@ -292,6 +292,6 @@ const openUrl = (url: string) => {
} }
a { a {
@apply cursor-pointer text-3xl rounded-full p-2 bg-gray-100 shadow-md hover:(shadow-lg bg-gray-200) transition-color ease-in duration-100; @apply cursor-pointer text-3xl rounded-full p-2 bg-gray-100 shadow-md hover:(shadow-lg bg-gray-200);
} }
</style> </style>

2
packages/nc-gui-v2/components/general/Social.vue

@ -46,7 +46,7 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
<style scoped> <style scoped>
.icon { .icon {
@apply cursor-pointer text-3xl rounded-full p-2 bg-gray-100 shadow-md hover:(shadow-lg bg-gray-200) transition-color ease-in duration-100; @apply cursor-pointer text-3xl rounded-full p-2 bg-gray-100 shadow-md hover:(shadow-lg bg-gray-200);
} }
.discourse { .discourse {

6
packages/nc-gui-v2/components/general/Sponsors.vue

@ -28,3 +28,9 @@ const { nav = false } = defineProps<Props>()
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>
<style>
a img {
margin: 0 !important;
}
</style>

10
packages/nc-gui-v2/layouts/default.vue

@ -18,11 +18,11 @@ export default {
</script> </script>
<template> <template>
<v-main> <a-layout-content>
<slot name="sidebar"> <teleport v-if="$slots.sidebar" to="#sidebar">
<div id="sidebar" /> <slot name="sidebar" />
</slot> </teleport>
<slot /> <slot />
</v-main> </a-layout-content>
</template> </template>

7
packages/nc-gui-v2/nuxt.config.ts

@ -2,8 +2,8 @@ import path from 'path'
import { defineNuxtConfig } from 'nuxt' import { defineNuxtConfig } from 'nuxt'
import vueI18n from '@intlify/vite-plugin-vue-i18n' import vueI18n from '@intlify/vite-plugin-vue-i18n'
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({ export default defineNuxtConfig({
@ -19,7 +19,7 @@ export default defineNuxtConfig({
'~/assets/css/global.css', '~/assets/css/global.css',
'~/assets/style/style.css', '~/assets/style/style.css',
'~/assets/style.css', '~/assets/style.css',
'~/assets/style-v2.css', '~/assets/style-v2.scss',
], ],
meta: { meta: {
@ -53,6 +53,7 @@ export default defineNuxtConfig({
Icons({ Icons({
autoInstall: true, autoInstall: true,
compiler: 'vue3', compiler: 'vue3',
defaultClass: 'nc-icon',
}), }),
Components({ Components({
resolvers: [AntDesignVueResolver()], resolvers: [AntDesignVueResolver()],

19
packages/nc-gui-v2/package-lock.json generated

@ -18,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.25.2", "@antfu/eslint-config": "^0.25.2",
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/material-symbols": "^1.1.8", "@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25", "@iconify-json/mdi": "^1.1.25",
"@intlify/vite-plugin-vue-i18n": "^4.0.0", "@intlify/vite-plugin-vue-i18n": "^4.0.0",
@ -952,6 +953,15 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/@iconify-json/clarity": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/clarity/-/clarity-1.1.4.tgz",
"integrity": "sha512-WnSp4vqVBDqoTvJqUsp39Vo+39iHsPckpLcKmlA5yKxghbt7C3nd9Twy4+WGFuVYq/p2Jy2PFr6EWqciFJSbNw==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/material-symbols": { "node_modules/@iconify-json/material-symbols": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.1.8.tgz",
@ -14669,6 +14679,15 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"@iconify-json/clarity": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@iconify-json/clarity/-/clarity-1.1.4.tgz",
"integrity": "sha512-WnSp4vqVBDqoTvJqUsp39Vo+39iHsPckpLcKmlA5yKxghbt7C3nd9Twy4+WGFuVYq/p2Jy2PFr6EWqciFJSbNw==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/material-symbols": { "@iconify-json/material-symbols": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.1.8.tgz",

1
packages/nc-gui-v2/package.json

@ -24,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.25.2", "@antfu/eslint-config": "^0.25.2",
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/material-symbols": "^1.1.8", "@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25", "@iconify-json/mdi": "^1.1.25",
"@intlify/vite-plugin-vue-i18n": "^4.0.0", "@intlify/vite-plugin-vue-i18n": "^4.0.0",

128
packages/nc-gui-v2/pages/forgot-password.vue

@ -0,0 +1,128 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { definePageMeta } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MdiLogin from '~icons/mdi/login'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import ClaritySuccessLine from '~icons/clarity/success-line'
const { $api } = $(useNuxtApp())
const { t } = useI18n()
definePageMeta({
requiresAuth: false,
title: 'title.resetPassword',
})
let error = $ref<string | null>(null)
let success = $ref(false)
const valid = ref()
const formValidator = ref()
const form = reactive({
email: '',
})
const formRules = {
email: [
// E-mail is required
(v: string) => !!v || t('msg.error.signUpRules.emailReqd'),
// E-mail must be valid format
(v: string) => isEmail(v) || t('msg.error.signUpRules.emailInvalid'),
],
}
const resetPassword = async () => {
error = null
try {
await $api.auth.passwordForgot(form)
success = true
} catch (e: any) {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
error = await extractSdkResponseErrorMsg(e)
}
}
const resetError = () => {
if (error) {
error = null
}
}
</script>
<template>
<NuxtLayout>
<v-form
ref="formValidator"
v-model="valid"
class="h-full min-h-[600px] flex justify-center items-center"
@submit.prevent="resetPassword"
>
<div class="h-full w-full flex flex-col flex-wrap justify-center items-center">
<div
class="color-transition bg-white dark:(!bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon />
<div class="self-center flex flex-col justify-center items-center text-center gap-4">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<template v-if="!success">
<p class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</p>
<p class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</p>
</template>
<template v-else>
<p class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</p>
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</template>
</div>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
</div>
</Transition>
<v-text-field
id="email"
v-model="form.email"
class="bg-white dark:!bg-gray-900"
:rules="formRules.email"
:label="$t('labels.email')"
:placeholder="$t('labels.email')"
:persistent-placeholder="true"
type="text"
@focus="resetError"
/>
<div class="self-center flex flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<button
:disabled="!valid"
:class="[
!valid
? '!opacity-50 !cursor-default'
: 'text-white bg-primary hover:(text-primary !bg-primary/75) dark:(!bg-secondary/75 hover:!bg-secondary/50)',
]"
class="ml-1 border-1 border-solid border-gray-300 color-transition rounded-lg p-4 bg-gray-100/50"
type="submit"
>
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('activity.sendEmail') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</div>
</div>
</v-form>
</NuxtLayout>
</template>

13
packages/nc-gui-v2/pages/index.vue

@ -1,13 +0,0 @@
<script setup lang="ts">
import { useRouter } from '#app'
const router = useRouter()
router.replace('/projects')
</script>
<template>
<div class="container" />
</template>
<style lang="scss"></style>

150
packages/nc-gui-v2/pages/index/index.vue

@ -0,0 +1,150 @@
<script lang="ts" setup>
import { createVNode } from '@vue/runtime-core'
import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MaterialSymbolsFormatListBulletedRounded from '~icons/material-symbols/format-list-bulleted-rounded'
import MaterialSymbolsGridView from '~icons/material-symbols/grid-view'
import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline'
import MdiFolderOutline from '~icons/mdi/folder-outline'
import ExclamationCircleOutlined from '~icons/mdi/information-outline'
const navDrawerOptions = [
{
title: 'My NocoDB',
icon: MdiFolderOutline,
},
/* todo: implement the api and bring back the options below
{
title: "Shared With Me",
icon: MdiAccountGroup
},
{
title: "Recent",
icon: MdiClockOutline
},
{
title: "Starred",
icon: MdiStar
} */
]
const route = useRoute()
const { $api } = useNuxtApp()
const toast = useToast()
const response = await $api.project.list({})
const projects = $ref(response.list)
const activePage = $ref(navDrawerOptions[0].title)
const deleteProject = (project: ProjectType) => {
Modal.confirm({
title: 'Do you want to delete the project?',
// icon: createVNode(ExclamationCircleOutlined),
content: 'Some descriptions',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
try {
await $api.project.delete(project.id as string)
projects.splice(projects.indexOf(project), 1)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const visible = ref(true)
</script>
<template>
<NuxtLayout>
<template #sidebar>
<div class="flex flex-col h-full">
<div class="flex p-4">
<v-menu class="select-none">
<template #activator="{ props }">
<div
class="color-transition hover:(bg-gray-100 dark:bg-secondary/25) dark:(bg-secondary/50 !text-white shadow-gray-600) mr-auto select-none flex items-center gap-2 leading-8 cursor-pointer rounded-full border-1 border-gray-300 px-5 py-2 shadow prose-lg font-semibold"
@click="props.onClick"
>
<MdiPlus class="text-primary dark:(!text-white) text-2xl" />
{{ $t('title.newProj') }}
</div>
</template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2">
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/create')"
>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div>
<div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/create-external')"
>
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</v-list>
</v-menu>
</div>
<a-menu class="mx-4 dark:bg-gray-800 dark:text-white flex-1 border-0">
<a-menu-item
v-for="(option, index) in navDrawerOptions"
:key="index"
class="f!rounded-r-lg"
@click="activePage = option.title"
>
<div class="flex items-center gap-4">
<component :is="option.icon" />
<span class="font-semibold">
{{ option.title }}
</span>
</div>
</a-menu-item>
</a-menu>
<general-social />
<general-sponsors :nav="true" />
</div>
</template>
<v-container class="flex-1 mb-12">
<div class="flex">
<div class="flex-1 text-2xl md:text-4xl font-bold text-gray-500 dark:text-white p-4">
{{ activePage }}
</div>
<div class="self-end flex text-4xl mb-1">
<MaterialSymbolsGridView
:class="route.name === 'index-index' ? 'text-primary dark:(!text-secondary/75)' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/')"
/>
<MaterialSymbolsFormatListBulletedRounded
:class="route.name === 'index-index-list' ? 'text-primary dark:(!text-secondary/75)' : ''"
class="cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/list')"
/>
</div>
</div>
<a-divider class="!mb-4 lg:(!mb-8)" />
<NuxtPage :projects="projects" @delete-project="deleteProject" />
</v-container>
<a-modal></a-modal>
</NuxtLayout>
</template>

42
packages/nc-gui-v2/pages/projects/index/index.vue → packages/nc-gui-v2/pages/index/index/index.vue

@ -2,11 +2,11 @@
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app' import { navigateTo } from '#app'
import useColors from '~/composables/useColors' import useColors from '~/composables/useColors'
import MdiStarOutline from '~icons/mdi/star-outline'
import MdiMenuDown from '~icons/mdi/menu-down' import MdiMenuDown from '~icons/mdi/menu-down'
import MdiDeleteOutline from '~icons/mdi/delete-outline' import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline' import MdiDatabaseOutline from '~icons/mdi/database-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
interface Props { interface Props {
projects: ProjectType[] projects: ProjectType[]
@ -14,6 +14,8 @@ interface Props {
const { projects } = defineProps<Props>() const { projects } = defineProps<Props>()
const emit = defineEmits(['delete-project'])
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { getColorByIndex } = useColors(true) const { getColorByIndex } = useColors(true)
@ -48,16 +50,13 @@ const formatTitle = (title: string) =>
</div> </div>
</template> </template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2"> <v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2">
<div <div class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2" @click="navigateTo('/create')">
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/projects/create')"
>
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" /> <MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div> <div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div> </div>
<div <div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2" class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/projects/create-external')" @click="navigateTo('/create-external')"
> >
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" /> <MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" /> <div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
@ -69,21 +68,25 @@ const formatTitle = (title: string) =>
<div v-for="(project, i) of projects" :key="project.id" class="group flex flex-col items-center gap-2"> <div v-for="(project, i) of projects" :key="project.id" class="group flex flex-col items-center gap-2">
<div class="thumbnail" :style="{ '--thumbnail-color': getColorByIndex(i) }" @click="openProject(project)"> <div class="thumbnail" :style="{ '--thumbnail-color': getColorByIndex(i) }" @click="openProject(project)">
{{ formatTitle(project.title) }} {{ formatTitle(project.title) }}
<a-dropdown @click.stop>
<MdiStarOutline class="star-icon" @click.stop /> <MdiMenuDown class="menu-icon" />
<template #overlay>
<v-menu> <a-menu>
<template #activator="{ props }"> <a-menu-item @click.stop="emit('delete-project', project)">
<MdiMenuDown class="menu-icon" @click.stop="props.onClick" /> <div class="grid grid-cols-6 cursor-pointer flex items-center p-2">
</template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300">
<div class="grid grid-cols-6 cursor-pointer hover:bg-gray-200 flex items-center p-2" @click.stop>
<MdiDeleteOutline class="col-span-2 mr-1 mt-[1px] text-red text-lg" /> <MdiDeleteOutline class="col-span-2 mr-1 mt-[1px] text-red text-lg" />
<div class="col-span-4 text-sm xl:text-md">{{ $t('general.delete') }}</div> <div class="col-span-4 text-sm xl:text-md">{{ $t('general.delete') }}</div>
</div> </div>
</v-list> </a-menu-item>
</v-menu> <a-menu-item>
<div class="grid grid-cols-6 cursor-pointer flex items-center p-2">
<MdiEditOutline class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-4 text-sm xl:text-md">{{ $t('general.edit') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div> </div>
<div class="prose-lg font-semibold"> <div class="prose-lg font-semibold">
@ -95,7 +98,7 @@ const formatTitle = (title: string) =>
<style scoped> <style scoped>
.thumbnail { .thumbnail {
@apply relative rounded-md opacity-75 font-bold text-white text-[75px] h-[150px] w-full max-w-[150px] shadow-md cursor-pointer uppercase flex items-center justify-center transition-color ease-in duration-100 hover:(after:opacity-100 shadow-none); @apply relative rounded-md opacity-75 font-bold text-white text-[75px] h-[150px] w-full max-w-[150px] shadow-md cursor-pointer uppercase flex items-center justify-center color-transition hover:(after:opacity-100 shadow-none);
} }
.thumbnail::after { .thumbnail::after {
@ -104,6 +107,7 @@ const formatTitle = (title: string) =>
content: ''; content: '';
z-index: -1; z-index: -1;
} }
.thumbnail:hover::after { .thumbnail:hover::after {
@apply shadow-2xl transform scale-110; @apply shadow-2xl transform scale-110;
} }

18
packages/nc-gui-v2/pages/projects/index/list.vue → packages/nc-gui-v2/pages/index/index/list.vue

@ -2,12 +2,17 @@
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app' import { navigateTo } from '#app'
import MdiDeleteOutline from '~icons/mdi/delete-outline'
import MdiEditOutline from '~icons/mdi/edit-outline'
interface Props { interface Props {
projects: ProjectType[] projects: ProjectType[]
} }
const { projects } = defineProps<Props>() const { projects } = defineProps<Props>()
const emit = defineEmits(['delete-project'])
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const openProject = async (project: ProjectType) => { const openProject = async (project: ProjectType) => {
@ -17,23 +22,26 @@ const openProject = async (project: ProjectType) => {
</script> </script>
<template> <template>
<div> <div class="mx-auto max-w-[700px]">
<div class="grid grid-cols-3 gap-2 prose-md p-2 font-semibold"> <div class="grid grid-cols-4 gap-2 prose-md p-2 font-semibold">
<div>{{ $t('general.title') }}</div> <div>{{ $t('general.title') }}</div>
<div>Status</div>
<div>Updated At</div> <div>Updated At</div>
<div></div>
</div> </div>
<v-divider class="col-span-3" /> <v-divider class="col-span-3" />
<template v-for="project of projects" :key="project.id"> <template v-for="project of projects" :key="project.id">
<div <div
class="cursor-pointer grid grid-cols-3 gap-2 prose-md hover:(bg-gray-100 shadow-sm dark:text-black) p-2 transition-color ease-in duration-100" class="cursor-pointer grid grid-cols-4 gap-2 prose-md hover:(bg-gray-100 shadow-sm dark:text-black) p-2 transition-color ease-in duration-100"
@click="openProject(project)" @click="openProject(project)"
> >
<div class="font-semibold">{{ project.title || 'Untitled' }}</div> <div class="font-semibold">{{ project.title || 'Untitled' }}</div>
<div>{{ project.status }}</div>
<div>{{ project.updated_at }}</div> <div>{{ project.updated_at }}</div>
<div>
<MdiDeleteOutline class="text-gray-500 hover:text-red-500 mr-2" @click.stop @click="emit('delete-project', project)" />
<MdiEditOutline class="text-gray-500 hover:text-primary mr-2" @click.stop />
</div>
</div> </div>
<v-divider class="col-span-3" /> <v-divider class="col-span-3" />
</template> </template>

42
packages/nc-gui-v2/pages/index/user/index.vue

@ -0,0 +1,42 @@
<script setup lang="ts">
import { useNuxtApp, useRoute } from '#app'
import MdiAccountCog from '~icons/mdi/account-cog'
const { $api, $state } = useNuxtApp()
const route = useRoute()
</script>
<template>
<NuxtLayout>
<template #sidebar>
<v-navigation-drawer v-model="$state.sidebarOpen.value" :border="0">
<div class="flex flex-col h-full">
<div class="advance-menu flex-1">
<v-list class="flex flex-col gap-1" :color="$state.darkMode.value ? 'secondary' : 'primary'">
<v-list-item
:active="route.name === 'index-user-index'"
class="flex items-center gap-4 !rounded-r-lg"
:value="$t('activity.settings')"
>
<MdiAccountCog />
<span class="font-semibold">
{{ $t('activity.settings') }}
</span>
</v-list-item>
</v-list>
</div>
<v-divider />
<general-social />
<general-sponsors :nav="true" />
</div>
</v-navigation-drawer>
</template>
<NuxtPage />
</NuxtLayout>
</template>

140
packages/nc-gui-v2/pages/index/user/index/index.vue

@ -0,0 +1,140 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
import { reactive, ref } from '#imports'
const { $api, $state } = useNuxtApp()
const { t } = useI18n()
const valid = ref()
let error = $ref<string | null>(null)
const form = reactive({
currentPassword: '',
password: '',
passwordRepeat: '',
})
const formRules = {
currentPassword: [
(v: string) => !!v || t('msg.error.signUpRules.passwdRequired'),
// E-mail must be valid format
(v: string) => isEmail(v) || t('msg.error.signUpRules.emailInvalid'),
],
password: [
// Password is required
(v: string) => !!v || t('msg.error.signUpRules.passwdRequired'),
(v: string) => v.length >= 8 || t('msg.error.signUpRules.passwdLength'),
],
passwordRepeat: [
// Passwords match
(v: string) => v === form.password || t('msg.error.signUpRules.passwdMismatch'),
],
}
const passwordChange = async () => {
error = null
try {
const { msg } = await $api.auth.passwordChange(form)
console.log(msg)
} catch (e: any) {
error = await extractSdkResponseErrorMsg(e)
}
}
const resetError = () => {
if (error) {
error = null
}
}
</script>
<template>
<v-form
ref="formValidator"
v-model="valid"
class="h-[calc(100%_+_180px)] min-h-[600px] flex justify-center items-center"
@submit.prevent="passwordChange"
>
<div class="h-full w-full flex flex-col flex-wrap justify-center items-center">
<div
class="dark:(md:bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<div
style="left: -moz-calc(50% - 45px); left: -webkit-calc(50% - 45px); left: calc(50% - 45px)"
class="absolute top-12 md:top-[-10%] rounded-lg bg-primary"
>
<img width="90" height="90" src="~/assets/img/icons/512x512-trans.png" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signUp') }}</h1>
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
</div>
</Transition>
<v-text-field
id="email"
v-model="form.email"
class="bg-white dark:!bg-gray-900"
:rules="formRules.email"
:label="$t('labels.email')"
:placeholder="$t('labels.email')"
:persistent-placeholder="true"
type="text"
@focus="resetError"
/>
<v-text-field
id="password"
v-model="form.password"
class="bg-white dark:!bg-gray-900"
:rules="formRules.password"
:label="$t('labels.password')"
:placeholder="$t('labels.password')"
:persistent-placeholder="true"
type="password"
@focus="resetError"
/>
<v-text-field
id="password_repeat"
v-model="form.passwordRepeat"
class="bg-white dark:!bg-gray-900"
:rules="formRules.passwordRepeat"
:label="`Repeat ${$t('labels.password')}`"
:placeholder="`Repeat ${$t('labels.password')}`"
:persistent-placeholder="true"
type="password"
@focus="resetError"
/>
<div class="self-center flex flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<button
:disabled="!valid"
:class="[
!valid
? '!opacity-50 !cursor-default'
: 'shadow-md hover:(text-primary bg-primary/10 dark:text-white dark:!bg-primary/50)',
]"
class="ml-1 border-1 border-solid border-gray-300 color-transition rounded-lg p-4 bg-gray-100/50"
type="submit"
>
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</div>
</div>
</v-form>
</template>

2
packages/nc-gui-v2/pages/nc/[projectId].vue

@ -29,9 +29,7 @@ watch(
<template> <template>
<NuxtLayout> <NuxtLayout>
<template #sidebar> <template #sidebar>
<v-navigation-drawer permanent>
<DashboardTreeView /> <DashboardTreeView />
</v-navigation-drawer>
</template> </template>
<v-container fluid> <v-container fluid>

108
packages/nc-gui-v2/pages/projects/index.vue

@ -1,69 +1,94 @@
<script lang="ts" setup> <script lang="ts" setup>
import { createVNode } from '@vue/runtime-core'
import { Modal } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { useToast } from 'vue-toastification'
import { navigateTo } from '#app' import { navigateTo } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MaterialSymbolsFormatListBulletedRounded from '~icons/material-symbols/format-list-bulleted-rounded' import MaterialSymbolsFormatListBulletedRounded from '~icons/material-symbols/format-list-bulleted-rounded'
import MaterialSymbolsGridView from '~icons/material-symbols/grid-view' import MaterialSymbolsGridView from '~icons/material-symbols/grid-view'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
import MdiDatabaseOutline from '~icons/mdi/database-outline' import MdiDatabaseOutline from '~icons/mdi/database-outline'
import MdiFolderOutline from '~icons/mdi/folder-outline' import MdiFolderOutline from '~icons/mdi/folder-outline'
import MdiAccountGroup from '~icons/mdi/account-group' import ExclamationCircleOutlined from '~icons/mdi/information-outline'
import MdiClockOutline from '~icons/mdi/clock-outline'
import MdiStar from '~icons/mdi/star'
const navDrawerOptions = [ const navDrawerOptions = [
{ {
title: 'My NocoDB', title: 'My NocoDB',
icon: MdiFolderOutline, icon: MdiFolderOutline,
}, },
/* todo: implement the api and bring back the options below
{ {
title: 'Shared With Me', title: "Shared With Me",
icon: MdiAccountGroup, icon: MdiAccountGroup
}, },
{ {
title: 'Recent', title: "Recent",
icon: MdiClockOutline, icon: MdiClockOutline
}, },
{ {
title: 'Starred', title: "Starred",
icon: MdiStar, icon: MdiStar
}, } */
] ]
const route = useRoute() const route = useRoute()
const { $api, $state } = useNuxtApp() const { $api, $state } = useNuxtApp()
const toast = useToast()
const response = await $api.project.list({}) const response = await $api.project.list({})
const projects = $ref(response.list) const projects = $ref(response.list)
const activePage = $ref(navDrawerOptions[0].title) const activePage = $ref(navDrawerOptions[0].title)
const deleteProject = (project: ProjectType) => {
Modal.confirm({
title: 'Do you want to delete the project?',
// icon: createVNode(ExclamationCircleOutlined),
content: 'Some descriptions',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
try {
await $api.project.delete(project.id as string)
projects.splice(projects.indexOf(project), 1)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
const visible = ref(true)
</script> </script>
<template> <template>
<NuxtLayout> <NuxtLayout>
<template #sidebar> <template #sidebar>
<v-navigation-drawer v-model="$state.sidebarOpen.value" :border="0">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="flex p-4"> <div class="flex p-4">
<v-menu class="select-none"> <v-menu class="select-none">
<template #activator="{ props }"> <template #activator="{ props }">
<div <div
class="bg-white mr-auto select-none flex items-center gap-2 leading-8 cursor-pointer rounded-full border-1 border-gray-300 px-5 py-2 shadow prose-lg font-semibold hover:(!bg-gray-100)" class="color-transition hover:(bg-gray-100 dark:bg-secondary/25) dark:(bg-secondary/50 !text-white shadow-gray-600) mr-auto select-none flex items-center gap-2 leading-8 cursor-pointer rounded-full border-1 border-gray-300 px-5 py-2 shadow prose-lg font-semibold"
@click="props.onClick" @click="props.onClick"
> >
<MdiPlus class="text-primary text-2xl" /> <MdiPlus class="text-primary dark:(!text-white) text-2xl" />
{{ $t('title.newProj') }} {{ $t('title.newProj') }}
</div> </div>
</template> </template>
<v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2"> <v-list class="!py-0 flex flex-col bg-white rounded-lg shadow-md border-1 border-gray-300 mt-2 ml-2">
<div <div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2" class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/projects/create')" @click="navigateTo('/create')"
> >
<MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" /> <MdiPlus class="col-span-2 mr-1 mt-[1px] text-primary text-lg" />
<div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div> <div class="col-span-10 text-sm xl:text-md">{{ $t('activity.createProject') }}</div>
</div> </div>
<div <div
class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2" class="grid grid-cols-12 cursor-pointer hover:bg-gray-200 flex items-center p-2"
@click="navigateTo('/projects/create-external')" @click="navigateTo('/create-external')"
> >
<MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" /> <MdiDatabaseOutline class="col-span-2 mr-1 mt-[1px] text-green-500 text-lg" />
<div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" /> <div class="col-span-10 text-sm xl:text-md" v-html="$t('activity.createProjectExtended.extDB')" />
@ -72,36 +97,27 @@ const activePage = $ref(navDrawerOptions[0].title)
</v-menu> </v-menu>
</div> </div>
<div class="advance-menu flex-1"> <a-menu class="mx-4 dark:bg-gray-800 dark:text-white flex-1 border-0">
<v-list class="flex flex-col gap-1" :color="$state.darkMode.value ? 'default' : 'primary'"> <a-menu-item
<!-- todo: v-list-item-group doesn't seem to work with vuetify 3 yet ... --> v-for="(option, index) in navDrawerOptions"
<v-list-item :key="index"
v-for="item in navDrawerOptions" class="f!rounded-r-lg"
:key="item.title" @click="activePage = option.title"
class="flex items-center gap-4 !rounded-r-lg"
:value="item.title"
> >
<component :is="item.icon" /> <div class="flex items-center gap-4">
<component :is="option.icon" />
<span <span class="font-semibold">
class="font-semibold" {{ option.title }}
:class="{
'textColor--text text--lighten-2': item.title !== activePage,
}"
>
{{ item.title }}
</span> </span>
</v-list-item>
</v-list>
</div> </div>
</a-menu-item>
<v-divider /> </a-menu>
<general-social /> <general-social />
<general-sponsors :nav="true" /> <general-sponsors :nav="true" />
</div> </div>
</v-navigation-drawer>
</template> </template>
<v-container class="flex-1 mb-12"> <v-container class="flex-1 mb-12">
@ -111,22 +127,24 @@ const activePage = $ref(navDrawerOptions[0].title)
</div> </div>
<div class="self-end flex text-4xl mb-1"> <div class="self-end flex text-4xl mb-1">
<MaterialSymbolsFormatListBulletedRounded
:class="route.name === 'projects-index-list' ? 'text-primary' : ''"
class="transition-color ease-in duration-100 cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/projects/list')"
/>
<MaterialSymbolsGridView <MaterialSymbolsGridView
:class="route.name === 'projects-index' ? 'text-primary' : ''" :class="route.name === 'projects-index' ? 'text-primary dark:(!text-secondary/75)' : ''"
class="transition-color ease-in duration-100 cursor-pointer p-2 hover:bg-gray-300/50 rounded-full" class="color-transition cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/projects')" @click="navigateTo('/projects')"
/> />
<MaterialSymbolsFormatListBulletedRounded
:class="route.name === 'projects-index-list' ? 'text-primary dark:(!text-secondary/75)' : ''"
class="color-transition cursor-pointer p-2 hover:bg-gray-300/50 rounded-full"
@click="navigateTo('/projects/list')"
/>
</div> </div>
</div> </div>
<v-divider class="!mb-4 lg:(!mb-8)" /> <a-divider class="!mb-4 lg:(!mb-8)" />
<NuxtPage :projects="projects" /> <NuxtPage :projects="projects" @delete-project="deleteProject" />
</v-container> </v-container>
<a-modal></a-modal>
</NuxtLayout> </NuxtLayout>
</template> </template>

33
packages/nc-gui-v2/pages/signin.vue

@ -55,7 +55,7 @@ const signIn = async () => {
try { try {
const { token } = await $api.auth.signin(form) const { token } = await $api.auth.signin(form)
$state.signIn(token!) $state.signIn(token!)
await navigateTo('/projects') await navigateTo('/')
} catch (e: any) { } catch (e: any) {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages) // todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
error = await extractSdkResponseErrorMsg(e) error = await extractSdkResponseErrorMsg(e)
@ -74,19 +74,14 @@ const resetError = () => {
<v-form <v-form
ref="formValidator" ref="formValidator"
v-model="valid" v-model="valid"
class="h-full min-h-[600px] flex justify-center items-center" class="h-[calc(100%_+_90px)] min-h-[600px] flex justify-center items-center"
@submit.prevent="signIn" @submit.prevent="signIn"
> >
<div class="h-full w-full flex flex-col flex-wrap justify-center items-center"> <div class="h-full w-full flex flex-col flex-wrap justify-center items-center">
<div <div
class="bg-white dark:(!bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)" class="bg-white dark:(!bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
> >
<div <general-noco-icon />
style="left: -moz-calc(50% - 45px); left: -webkit-calc(50% - 45px); left: calc(50% - 45px)"
class="absolute top-12 md:top-[-10%] rounded-lg bg-primary"
>
<img width="90" height="90" src="~/assets/img/icons/512x512-trans.png" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1> <h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1>
@ -121,7 +116,7 @@ const resetError = () => {
/> />
<div class="hidden md:block self-end mx-8"> <div class="hidden md:block self-end mx-8">
<nuxt-link class="prose-sm text-primary underline hover:opacity-75" to="/forgot-password"> <nuxt-link class="prose-sm" to="/forgot-password">
{{ $t('msg.info.signUp.forgotPassword') }} {{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link> </nuxt-link>
</div> </div>
@ -132,20 +127,20 @@ const resetError = () => {
:class="[ :class="[
!valid !valid
? '!opacity-50 !cursor-default' ? '!opacity-50 !cursor-default'
: 'shadow-md hover:(text-primary bg-primary/10 dark:text-white dark:!bg-primary/50)', : 'text-white bg-primary hover:(text-primary !bg-primary/75) dark:(!bg-secondary/75 hover:!bg-secondary/50)',
]" ]"
class="ml-1 border-1 border-solid border-gray-300 transition-color duration-100 ease-in rounded-lg p-4 bg-gray-100/50" class="ml-1 border-1 border-solid border-gray-300 rounded-lg p-4 bg-gray-100/50"
type="submit" type="submit"
> >
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span> <span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span>
</button> </button>
<div class="text-end prose-sm"> <div class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }} {{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link class="text-primary underline hover:opacity-75" to="/signup">{{ $t('general.signUp') }}</nuxt-link> <nuxt-link to="/signup">{{ $t('general.signUp') }}</nuxt-link>
</div> </div>
<div class="prose-sm md:hidden"> <div class="md:hidden">
<nuxt-link class="prose-sm text-primary underline hover:opacity-75" to="/forgot-password"> <nuxt-link class="prose-sm" to="/forgot-password">
{{ $t('msg.info.signUp.forgotPassword') }} {{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link> </nuxt-link>
</div> </div>
@ -155,13 +150,3 @@ const resetError = () => {
</v-form> </v-form>
</NuxtLayout> </NuxtLayout>
</template> </template>
<style lang="scss">
.v-field__field {
@apply bg-white dark:(!bg-gray-900 text-white);
input {
@apply bg-white dark:(!bg-gray-700) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;
}
}
</style>

17
packages/nc-gui-v2/pages/signup.vue

@ -46,7 +46,7 @@ const signUp = async () => {
try { try {
const { token } = await $api.auth.signup(form) const { token } = await $api.auth.signup(form)
$state.signIn(token!) $state.signIn(token!)
await navigateTo('/projects') await navigateTo('/')
} catch (e: any) { } catch (e: any) {
error = await extractSdkResponseErrorMsg(e) error = await extractSdkResponseErrorMsg(e)
} }
@ -71,12 +71,7 @@ const resetError = () => {
<div <div
class="bg-white dark:(!bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)" class="bg-white dark:(!bg-gray-900 !text-white) md:relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
> >
<div <general-noco-icon />
style="left: -moz-calc(50% - 45px); left: -webkit-calc(50% - 45px); left: calc(50% - 45px)"
class="absolute top-12 md:top-[-10%] rounded-lg bg-primary"
>
<img width="90" height="90" src="~/assets/img/icons/512x512-trans.png" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signUp') }}</h1> <h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signUp') }}</h1>
@ -122,22 +117,22 @@ const resetError = () => {
@focus="resetError" @focus="resetError"
/> />
<div class="self-center flex items-center justify-between w-full"> <div class="self-center flex flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<button <button
:disabled="!valid" :disabled="!valid"
:class="[ :class="[
!valid !valid
? '!opacity-50 !cursor-default' ? '!opacity-50 !cursor-default'
: 'shadow-md hover:(text-primary bg-primary/10 dark:text-white dark:!bg-primary/50)', : 'text-white bg-primary hover:(text-primary !bg-primary/75) dark:(!bg-secondary/75 hover:!bg-secondary/50)',
]" ]"
class="ml-1 border-1 border-solid border-gray-300 transition-color duration-100 ease-in rounded-lg p-4 bg-gray-100/50" class="ml-1 border-1 border-solid border-gray-300 rounded-lg p-4 bg-gray-100/50"
type="submit" type="submit"
> >
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span> <span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button> </button>
<div class="text-end prose-sm"> <div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }} {{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link class="text-primary underline hover:opacity-75" to="/signin">{{ $t('general.signIn') }}</nuxt-link> <nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div> </div>
</div> </div>
</div> </div>

8
packages/nc-gui-v2/windi.config.ts

@ -15,12 +15,10 @@ import colors, { themeColors } from './utils/colorsUtils'
export default defineConfig({ export default defineConfig({
extract: { extract: {
include: ['**/*.{vue,html,jsx,tsx}'], include: ['**/*.{vue,html,jsx,tsx,css,scss}'],
exclude: ['node_modules', '.git'], exclude: ['node_modules', '.git'],
}, },
attributify: true,
darkMode: 'class', darkMode: 'class',
plugins: [ plugins: [
@ -43,7 +41,9 @@ export default defineConfig({
}, },
shortcuts: { shortcuts: {
'scrollbar-thin-primary': 'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white', 'color-transition': 'transition-color duration-100 ease-in',
'scrollbar-thin-primary':
'scrollbar scrollbar-thin scrollbar-thumb-rounded scrollbar-thumb-primary scrollbar-track-white dark:(!scrollbar-track-black)',
}, },
theme: { theme: {

Loading…
Cancel
Save