Browse Source

Merge pull request #3933 from nocodb/refactor/user-page

pull/3945/head
Braks 2 years ago committed by GitHub
parent
commit
70d3d1f53f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  2. 4
      packages/nc-gui/composables/useProject.ts
  3. 9
      packages/nc-gui/layouts/base.vue
  4. 1
      packages/nc-gui/pages/[projectType]/[projectId]/index.vue
  5. 2
      packages/nc-gui/pages/index/index.vue
  6. 27
      packages/nc-gui/pages/index/index/[projectId].vue
  7. 8
      packages/nc-gui/pages/index/index/create-external.vue
  8. 4
      packages/nc-gui/pages/index/index/create.vue
  9. 2
      packages/nc-gui/pages/index/index/index.vue
  10. 158
      packages/nc-gui/pages/index/index/user.vue
  11. 142
      packages/nc-gui/pages/index/user/index.vue
  12. 16
      packages/nc-gui/pages/signin.vue
  13. 2
      packages/nc-gui/pages/signup/[[token]].vue
  14. 63
      scripts/cypress/integration/common/6h_change_password.js
  15. 4
      scripts/cypress/integration/test/restMisc.js
  16. 4
      scripts/cypress/integration/test/xcdb-restMisc.js

1
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -17,7 +17,6 @@ import {
useDialog,
useI18n,
useNuxtApp,
useRoute,
useRouter,
viewTypeAlias,
watch,

4
packages/nc-gui/composables/useProject.ts

@ -79,7 +79,7 @@ const [setup, use] = useInjectionState(() => {
}
}
async function loadProject() {
async function loadProject(withTheme = true) {
if (projectType === 'base') {
try {
const baseData = await api.public.sharedBaseGet(route.params.projectId as string)
@ -102,7 +102,7 @@ const [setup, use] = useInjectionState(() => {
await loadTables()
setTheme(projectMeta.value?.theme)
if (withTheme) setTheme(projectMeta.value?.theme)
return projectLoadedHook.trigger(project.value)
}

9
packages/nc-gui/layouts/base.vue

@ -44,6 +44,7 @@ hooks.hook('page:finish', () => {
<div
v-if="!route.params.projectType"
v-e="['c:navbar:home']"
data-cy="nc-noco-brand-icon"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
@ -77,11 +78,15 @@ hooks.hook('page:finish', () => {
<template v-if="signedIn">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-user-accounts-menu">
<MdiDotsVertical class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white" @click.prevent />
<MdiDotsVertical
data-cy="nc-menu-accounts"
class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white"
@click.prevent
/>
<template #overlay>
<a-menu class="!py-0 leading-8 !rounded">
<a-menu-item key="0" class="!rounded-t">
<a-menu-item key="0" data-cy="nc-menu-accounts__user-settings" class="!rounded-t">
<nuxt-link v-e="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp;

1
packages/nc-gui/pages/[projectType]/[projectId]/index.vue

@ -192,6 +192,7 @@ onBeforeUnmount(reset)
<div
v-if="isOpen && !isSharedBase"
v-e="['c:navbar:home']"
data-cy="nc-noco-brand-icon"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>

2
packages/nc-gui/pages/index/index.vue

@ -9,7 +9,7 @@ useSidebar('nc-left-sidebar', { hasSidebar: false })
<template>
<NuxtLayout>
<div
class="min-h-[calc(100vh_-_var(--header-height))] h-auto bg-primary bg-opacity-5 flex flex-col lg:flex-row flex-wrap gap-6 py-6 px-12 pt-65px"
class="min-h-[calc(100vh_-_var(--header-height))] bg-primary bg-opacity-5 flex flex-wrap justify-between xl:flex-nowrap gap-6 py-6 px-4 pt-65px md:(px-12)"
>
<div class="flex-1 justify-end hidden xl:(flex)">
<div>

27
packages/nc-gui/pages/index/index/[projectId].vue

@ -5,23 +5,19 @@ import {
extractSdkResponseErrorMsg,
message,
navigateTo,
nextTick,
onMounted,
projectTitleValidator,
reactive,
ref,
tryOnMounted,
useProject,
useRoute,
useSidebar,
} from '#imports'
useSidebar('nc-left-sidebar', { hasSidebar: false })
const route = useRoute()
const { project, loadProject, updateProject, isLoading } = useProject()
const { project, loadProject, updateProject, isLoading, projectLoadedHook } = useProject()
await loadProject()
loadProject(false)
const nameValidationRules = [
{
@ -48,30 +44,30 @@ const renameProject = async () => {
}
// select and focus title field on load
onMounted(async () => {
projectLoadedHook(async () => {
formState.title = project.value.title as string
await nextTick(() => {
tryOnMounted(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title?.length)
input.focus()
}, 500)
input.setSelectionRange(0, formState.title?.length)
}, 150)
})
})
</script>
<template>
<div
class="update-project bg-white relative flex-auto flex flex-col justify-center gap-2 p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
class="update-project relative flex-auto flex flex-col justify-center gap-2 p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
@ -79,7 +75,10 @@ onMounted(async () => {
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('activity.editProject') }}</h1>
<a-skeleton v-if="isLoading" />
<a-form
v-else
ref="form"
:model="formState"
name="basic"

8
packages/nc-gui/pages/index/index/create-external.vue

@ -335,12 +335,12 @@ onMounted(async () => {
<template>
<div
class="create-external bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
class="create-external relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
@ -367,8 +367,8 @@ onMounted(async () => {
dropdown-class-name="nc-dropdown-ext-db-type"
@change="onClientChange"
>
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value">
{{ client.text }}
</a-select-option>
</a-select>
</a-form-item>

4
packages/nc-gui/pages/index/index/create.vue

@ -64,12 +64,12 @@ onMounted(async () => {
<template>
<div
class="create bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
class="create relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />

2
packages/nc-gui/pages/index/index/index.vue

@ -126,7 +126,7 @@ onBeforeMount(loadProjects)
</script>
<template>
<div class="bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)">
<div class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)">
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8 mt-4">
<!-- My Projects -->
<span class="text-4xl nc-project-page-title">{{ $t('title.myProject') }}</span>

158
packages/nc-gui/pages/index/index/user.vue

@ -0,0 +1,158 @@
<script lang="ts" setup>
import { message, navigateTo, reactive, ref, useApi, useGlobal, useI18n, useRouter } from '#imports'
const router = useRouter()
const { api, error, isLoading } = useApi({ useGlobalInstance: true })
const { t } = useI18n()
const { signOut } = useGlobal()
const formValidator = ref()
const form = reactive({
currentPassword: '',
password: '',
passwordRepeat: '',
})
const formRules = {
currentPassword: [
// Current password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
passwordRepeat: [
// PasswordRepeat is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
// Passwords match
{
validator: (_: unknown, _v: string) => {
return new Promise((resolve, reject) => {
if (form.password === form.passwordRepeat) return resolve(true)
reject(new Error(t('msg.error.signUpRules.passwdMismatch')))
})
},
message: t('msg.error.signUpRules.passwdMismatch'),
},
],
}
const passwordChange = async () => {
const valid = formValidator.value.validate()
if (!valid) return
error.value = null
await api.auth.passwordChange({
currentPassword: form.currentPassword,
newPassword: form.password,
})
message.success(t('msg.success.passwordChanged'))
signOut()
navigateTo('/signin')
}
const resetError = () => {
if (error.value) error.value = null
}
</script>
<template>
<div class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)">
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full cursor-pointer"
@click="() => router.back()"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('activity.changePwd') }}</h1>
<a-form
ref="formValidator"
data-cy="nc-user-settings-form"
layout="vertical"
class="change-password lg:max-w-3/4 w-full !mx-auto"
no-style
:model="form"
@finish="passwordChange"
>
<Transition name="layout">
<div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div data-cy="nc-user-settings-form__error" class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
{{ error }}
</div>
</div>
</Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
data-cy="nc-user-settings-form__current-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-cy="nc-user-settings-form__new-password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
data-cy="nc-user-settings-form__new-password-repeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="text-center">
<button data-cy="nc-user-settings-form__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiKeyChange />
{{ $t('activity.changePwd') }}
</span>
</button>
</div>
</a-form>
</div>
</template>
<style lang="scss">
.change-password {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
.password {
input {
@apply !border-none !m-0;
}
}
}
</style>

142
packages/nc-gui/pages/index/user/index.vue

@ -1,142 +0,0 @@
<script lang="ts" setup>
import { message, navigateTo, reactive, ref, useApi, useGlobal, useI18n } from '#imports'
const { api, error } = useApi({ useGlobalInstance: true })
const { t } = useI18n()
const { signOut } = useGlobal()
const formValidator = ref()
const form = reactive({
currentPassword: '',
password: '',
passwordRepeat: '',
})
const formRules = {
currentPassword: [
// Current password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
passwordRepeat: [
// PasswordRepeat is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
// Passwords match
{
validator: (_: unknown, _v: string) => {
return new Promise((resolve, reject) => {
if (form.password === form.passwordRepeat) return resolve(true)
reject(new Error(t('msg.error.signUpRules.passwdMismatch')))
})
},
message: t('msg.error.signUpRules.passwdMismatch'),
},
],
}
const passwordChange = async () => {
const valid = formValidator.value.validate()
if (!valid) return
error.value = null
await api.auth.passwordChange({
currentPassword: form.currentPassword,
newPassword: form.password,
})
message.success(t('msg.success.passwordChanged'))
signOut()
navigateTo('/signin')
}
const resetError = () => {
if (error.value) error.value = null
}
</script>
<template>
<NuxtLayout>
<div class="mt-4 w-1/2 mx-auto">
<a-form ref="formValidator" layout="vertical" :model="form" class="change-password" @finish="passwordChange">
<div class="md:relative flex flex-col gap-2 w-full h-full p-8 w-full">
<h1 class="prose-2xl font-bold mb-4">{{ $t('activity.changePwd') }}</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>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword">
<a-input-password
v-model:value="form.currentPassword"
size="large"
class="password"
:placeholder="$t('placeholder.password.current')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('placeholder.password.new')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat">
<a-input-password
v-model:value="form.passwordRepeat"
size="large"
class="password"
:placeholder="$t('placeholder.password.confirm')"
@focus="resetError"
/>
</a-form-item>
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiKeyChange />
{{ $t('activity.changePwd') }}
</span>
</button>
</div>
</div>
</a-form>
</div>
</NuxtLayout>
</template>
<style lang="scss">
.change-password {
@apply border-1 shadow-md rounded;
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
.password {
input {
@apply !border-none;
}
}
}
</style>

16
packages/nc-gui/pages/signin.vue

@ -63,7 +63,10 @@ function resetError() {
<template>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin">
<div
data-cy="nc-form-signin"
class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin"
>
<div
class="bg-white mt-[60px] 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)"
>
@ -82,12 +85,19 @@ function resetError() {
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input v-model:value="form.email" size="large" :placeholder="$t('msg.info.signUp.workEmail')" @focus="resetError" />
<a-input
v-model:value="form.email"
data-cy="nc-form-signin__email"
size="large"
:placeholder="$t('msg.info.signUp.workEmail')"
@focus="resetError"
/>
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
data-cy="nc-form-signin__password"
size="large"
class="password"
:placeholder="$t('msg.info.signUp.enterPassword')"
@ -102,7 +112,7 @@ function resetError() {
</div>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<button class="scaling-btn bg-opacity-100" type="submit">
<button data-cy="nc-form-signin__submit" class="scaling-btn bg-opacity-100" type="submit">
<span class="flex items-center gap-2">
<MdiLogin />
{{ $t('general.signIn') }}

2
packages/nc-gui/pages/signup/[[token]].vue

@ -85,7 +85,7 @@ function resetError() {
<div
class="bg-white mt-[60px] 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)"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent) hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent ring-opacity-100)" :animate="isLoading" />
<h1 class="prose-2xl font-bold self-center my-4">
{{ $t('general.signUp') }}

63
scripts/cypress/integration/common/6h_change_password.js

@ -0,0 +1,63 @@
import { isTestSuiteActive, roles } from "../../support/page_objects/projectConstants";
const newPassword = `${roles.owner.credentials.password}1`;
const currentPasswordIsWrong = "Current password is wrong";
const passwordsNotMatching = "Passwords do not match";
export const genTest = (apiType, dbType) => {
if (!isTestSuiteActive(apiType, dbType)) return;
describe('User settings', () => {
it('Visit user settings page', () => {
cy.get("[data-cy='nc-noco-brand-icon']").click();
cy.get("[data-cy='nc-menu-accounts']").click();
cy.get("[data-cy='nc-menu-accounts__user-settings']").click();
cy.get("[data-cy='nc-user-settings-form']").should("exist");
});
describe('Update password and verify user settings form validation', () => {
beforeEach(() => {
cy.get("[data-cy='nc-user-settings-form__current-password']").clear();
cy.get("[data-cy='nc-user-settings-form__new-password']").clear();
cy.get("[data-cy='nc-user-settings-form__new-password-repeat']").clear();
})
it('Verifies current password', () => {
cy.get("[data-cy='nc-user-settings-form__current-password']").type('WrongPassword');
cy.get("[data-cy='nc-user-settings-form__new-password']").type(newPassword);
cy.get("[data-cy='nc-user-settings-form__new-password-repeat']").type(newPassword);
cy.get("[data-cy='nc-user-settings-form__submit']").click();
cy.get("[data-cy='nc-user-settings-form__error']").should("exist").should("contain", currentPasswordIsWrong);
});
it('Verifies passwords match', () => {
cy.get("[data-cy='nc-user-settings-form__current-password']").type(roles.owner.credentials.password);
cy.get("[data-cy='nc-user-settings-form__new-password']").type(newPassword);
cy.get("[data-cy='nc-user-settings-form__new-password-repeat']").type(`${newPassword}NotMatching`);
cy.get("[data-cy='nc-user-settings-form__submit']").click();
cy.get(".ant-form-item-explain-error").should("exist").should("contain", passwordsNotMatching);
});
it('Changes user password & signs out', () => {
cy.get("[data-cy='nc-user-settings-form__current-password']").type(roles.owner.credentials.password);
cy.get("[data-cy='nc-user-settings-form__new-password']").type(newPassword);
cy.get("[data-cy='nc-user-settings-form__new-password-repeat']").type(newPassword);
cy.get("[data-cy='nc-user-settings-form__submit']").click();
cy.get("[data-cy='nc-user-settings-form__submit']").should("not.exist");
});
})
describe('Sign in with new password', () => {
it('Verifies new password works', () => {
cy.get("[data-cy='nc-form-signin']").should("exist");
cy.get("[data-cy='nc-form-signin__email']").type(roles.owner.credentials.username);
cy.get("[data-cy='nc-form-signin__password']").type(newPassword);
cy.get("[data-cy='nc-form-signin__submit']").click();
cy.get("[data-cy='nc-menu-accounts']").should("exist");
})
})
});
};

4
scripts/cypress/integration/test/restMisc.js

@ -5,6 +5,7 @@ let t6c = require("../common/6c_swagger_api");
let t6e = require("../common/6e_project_operations");
let t6f = require("../common/6f_attachments");
let t6g = require("../common/6g_base_share");
let t6h = require("../common/6h_change_password");
let t7a = require("../common/7a_create_project_from_excel");
let t8a = require("../common/8a_webhook");
let t9b = require("../common/9b_ERD");
@ -44,6 +45,9 @@ const nocoTestSuite = (apiType, dbType) => {
// Create project from Excel
t7a.genTest(apiType, dbType);
// Change password
t6h.genTest(apiType, dbType);
};
nocoTestSuite("rest", "mysql");

4
scripts/cypress/integration/test/xcdb-restMisc.js

@ -6,6 +6,7 @@ let t6d = require("../common/6d_language_validation");
let t6e = require("../common/6e_project_operations");
let t6f = require("../common/6f_attachments");
let t6g = require("../common/6g_base_share");
let t6h = require("../common/6h_change_password");
let t7a = require("../common/7a_create_project_from_excel");
let t8a = require("../common/8a_webhook");
const t9b = require("../common/9b_ERD");
@ -46,6 +47,9 @@ const nocoTestSuite = (apiType, dbType) => {
// Create project from Excel
t7a.genTest(apiType, dbType);
// Change password
t6h.genTest(apiType, dbType);
};
nocoTestSuite("rest", "xcdb");

Loading…
Cancel
Save