From 445714ab5ca217cd60c28f569ab5e2c81e709e61 Mon Sep 17 00:00:00 2001 From: Artemiy Vereshchinskiy Date: Sat, 22 Aug 2026 11:54:15 +0700 Subject: [PATCH 1/2] Minor UX upgrades --- .../src/dashboard/project/project.service.ts | 2 +- .../core/src/dashboard/user/user.service.ts | 47 ++++++++++- .../connect-guide/ProjectConnectionPanel.tsx | 8 +- .../projects/hooks/useProjectMutations.ts | 15 +--- .../records/components/ImportRecords.tsx | 7 +- .../tour/components/OnboardingTour.tsx | 19 ++++- .../src/features/tour/config/steps.tsx | 80 +------------------ .../src/features/tour/stores/tour.ts | 4 - platform/dashboard/src/features/tour/types.ts | 4 - platform/dashboard/src/pages/project/new.tsx | 22 +---- 10 files changed, 81 insertions(+), 127 deletions(-) diff --git a/platform/core/src/dashboard/project/project.service.ts b/platform/core/src/dashboard/project/project.service.ts index b84c91b0..98a6767a 100755 --- a/platform/core/src/dashboard/project/project.service.ts +++ b/platform/core/src/dashboard/project/project.service.ts @@ -46,7 +46,7 @@ export class ProjectService { } async createProject( - properties: CreateProjectDto, + properties: Partial, workspaceId: string, userId: string, _transaction?: Transaction diff --git a/platform/core/src/dashboard/user/user.service.ts b/platform/core/src/dashboard/user/user.service.ts index 27b9f256..10cd2c98 100644 --- a/platform/core/src/dashboard/user/user.service.ts +++ b/platform/core/src/dashboard/user/user.service.ts @@ -18,6 +18,7 @@ import { IDecodedResetToken } from '@/dashboard/auth/auth.types' import { ResetPasswordAuthDto } from '@/dashboard/auth/dto/reset-password-auth.dto' import { EncryptionService } from '@/dashboard/auth/encryption/encryption.service' import { ProjectService } from '@/dashboard/project/project.service' +import { TokenService } from '@/dashboard/token/token.service' import { ICreatedUserData } from '@/dashboard/user/interfaces/authenticated-user.interface' import { AcceptWorkspaceInvitationParams } from '@/dashboard/user/interfaces/user-properties.interface' import { @@ -37,6 +38,10 @@ import { User } from './user.entity' import type { UserRow } from '@/database/sql/schema/types' +const INITIAL_PROJECT_NAME = 'My first project' +const INITIAL_TOKEN_NAME = 'Initial' +const INITIAL_TOKEN_DESCRIPTION = 'Initial API Key to get you started quickly.' + @Injectable() export class UserService { constructor( @@ -46,7 +51,9 @@ export class UserService { @Inject(forwardRef(() => WorkspaceService)) private readonly workspaceService: WorkspaceService, @Inject(forwardRef(() => ProjectService)) - private readonly projectService: ProjectService + private readonly projectService: ProjectService, + @Inject(forwardRef(() => TokenService)) + private readonly tokenService: TokenService ) {} normalize(row?: UserRow): User | undefined { @@ -90,14 +97,48 @@ export class UserService { if (allowedLogins.length === 0 || allowedLogins.includes(properties.login)) { const userRow = await this.createUserNode(properties, transaction) - await this.workspaceService.createWorkspace({ name: 'Default Workspace' }, userRow.id, transaction) + const workspace = await this.workspaceService.createWorkspace( + { name: 'Default Workspace' }, + userRow.id, + transaction + ) + await this.createInitialProjectResources(userRow.id, workspace.toJson().id, transaction) - return { userData: this.normalize(userRow) } + return { userData: this.normalize(userRow), workspaceId: workspace.toJson().id } } else { throw new BadRequestException('Provided login is not allowed') } } + /** + * First-time setup only: every freshly registered user gets a starter project + * and an API key so they can reach the SDK in one click. This runs once at + * signup — deleting all projects later does NOT recreate them, because a user + * who already went through onboarding is no longer "fresh". + */ + private async createInitialProjectResources( + userId: string, + workspaceId: string, + transaction?: Transaction + ): Promise { + const project = await this.projectService.createProject( + { name: INITIAL_PROJECT_NAME }, + workspaceId, + userId, + transaction + ) + + await this.tokenService.createToken( + { + name: INITIAL_TOKEN_NAME, + description: INITIAL_TOKEN_DESCRIPTION, + expiration: '*' + }, + project.toJson().id, + transaction + ) + } + /** * Just-in-time user creation for Enterprise SSO. Unlike {@link create}, this * does NOT spin up a personal "Default Workspace" — the caller (SSO service) diff --git a/platform/dashboard/src/features/connect-guide/ProjectConnectionPanel.tsx b/platform/dashboard/src/features/connect-guide/ProjectConnectionPanel.tsx index 18218eb3..3bb08068 100644 --- a/platform/dashboard/src/features/connect-guide/ProjectConnectionPanel.tsx +++ b/platform/dashboard/src/features/connect-guide/ProjectConnectionPanel.tsx @@ -20,16 +20,21 @@ const maskToken = (value: string) => `${value.slice(0, 8)}${'•'.repeat(14)}` function ConnectionRow({ actions, children, + dataTour, icon, label }: { actions?: ReactNode children: ReactNode + dataTour?: string icon: ReactNode label: string }) { return ( -
+
{icon} {label} @@ -69,6 +74,7 @@ export function ProjectConnectionPanel({ loading, project }: { loading?: boolean
diff --git a/platform/dashboard/src/features/projects/hooks/useProjectMutations.ts b/platform/dashboard/src/features/projects/hooks/useProjectMutations.ts index 8769af09..fe1b126c 100644 --- a/platform/dashboard/src/features/projects/hooks/useProjectMutations.ts +++ b/platform/dashboard/src/features/projects/hooks/useProjectMutations.ts @@ -10,14 +10,6 @@ import { trackProjectCreated, trackApiKeyGenerated } from '~/lib/analytics' import { $currentWorkspaceId } from '~/features/workspaces/stores/current' import { $currentProjectId } from '~/features/projects/stores/id' import { $router, isProjectPage, redirectRoute } from '~/lib/router' -import { $tourAllowed, $tourStep, setTourStep } from '~/features/tour/stores/tour' - -const onboardingProjectCreationSteps = new Set([ - 'homeNewProject', - 'newProjectName', - 'newProjectCustomDb', - 'newProjectCreate' -]) export const useCreateProjectMutation = () => { const queryClient = useQueryClient() @@ -50,12 +42,7 @@ export const useCreateProjectMutation = () => { } const projectIsInactive = project.status === 'pending' || project.status === 'provisioning' if (!projectIsInactive) { - if ($tourAllowed.get() && onboardingProjectCreationSteps.has($tourStep.get())) { - setTourStep('projectImportRadio', true) - redirectRoute('projectImportData', { id: project.id }) - } else { - redirectRoute('projectHelp', { id: project.id }) - } + redirectRoute('projectHelp', { id: project.id }) } else { redirectRoute('projectSettings', { id: project.id }) } diff --git a/platform/dashboard/src/features/records/components/ImportRecords.tsx b/platform/dashboard/src/features/records/components/ImportRecords.tsx index ab8a330a..6d22e236 100644 --- a/platform/dashboard/src/features/records/components/ImportRecords.tsx +++ b/platform/dashboard/src/features/records/components/ImportRecords.tsx @@ -521,6 +521,11 @@ function isNDJSONorJSON(input: string): 'NDJSON' | 'JSON' | 'Unknown' { } } +function labelFromFileName(fileName: string): string { + const base = fileName.replace(/\.[^.]+$/, '').replace(/s$/i, '') + return base.toUpperCase() +} + type SupportedImportFileType = 'json' | 'jsonl' | 'ndjson' | 'csv' | 'unknown' function parseJsonLines(input: string): string { @@ -642,7 +647,7 @@ export function ImportRecords() { setUploadError(null) setLastDetectedType(detectedType.toUpperCase()) - $label.set('') + $label.set(labelFromFileName(file.name)) if (detectedType === 'csv') { $csvData.set(content) diff --git a/platform/dashboard/src/features/tour/components/OnboardingTour.tsx b/platform/dashboard/src/features/tour/components/OnboardingTour.tsx index 2fa10b41..8d86bccf 100644 --- a/platform/dashboard/src/features/tour/components/OnboardingTour.tsx +++ b/platform/dashboard/src/features/tour/components/OnboardingTour.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react' import type { CallBackProps } from 'react-joyride' import Joyride, { EVENTS, STATUS } from 'react-joyride' import { useStore } from '@nanostores/react' +import { useQuery } from '@tanstack/react-query' import { useUpdateUserMutation } from '~/features/auth/hooks/useAuthMutations' import type { routes } from '~/lib/router' import { $router, openRoute, projectRoutes } from '~/lib/router' @@ -18,6 +19,8 @@ import { $currentProjectId } from '~/features/projects/stores/id' import { useWaitForSelectorStable } from '~/features/tour/hooks/useWaitForSelector' import type { TourStepKey } from '~/features/tour/types' import type * as ConfettiModule from '@tsparticles/confetti' +import { workspaceProjectsQueryOptions } from '~/features/workspaces/queries/workspaceQueries' +import { $currentWorkspaceId } from '~/features/workspaces/stores/current' type TourStepData = { key: TourStepKey @@ -81,6 +84,12 @@ export function OnboardingTour() { const run = useStore($tourEffective) const { mutateAsync: updateSettings } = useUpdateUserMutation() const isAllowed = useStore($tourAllowed) + const workspaceId = useStore($currentWorkspaceId) + const { data: projects } = useQuery({ + ...workspaceProjectsQueryOptions(workspaceId), + enabled: isAllowed && Boolean(workspaceId) + }) + const fallbackProjectId = projects?.[0]?.id const currentStep = steps.find((step) => getStepData(step)?.key === currentKey) const targetSelector = typeof currentStep?.target === 'string' ? currentStep.target : '' @@ -129,8 +138,14 @@ export function OnboardingTour() { const nextKey = keys[index + 1] if (data.redirectTo) { const route = data.redirectTo - if (isProjectRouteName(route) && projectId) { - openRoute(route, { id: projectId }) + if (isProjectRouteName(route)) { + const targetProjectId = projectId ?? fallbackProjectId + if (targetProjectId) { + openRoute(route, { id: targetProjectId }) + } + // No project to land on (e.g. a returning user with all projects + // deleted): skip the redirect; the tour stays paused until the + // user opens a project page. } else { openRoute(route) } diff --git a/platform/dashboard/src/features/tour/config/steps.tsx b/platform/dashboard/src/features/tour/config/steps.tsx index 745bab2c..67a74336 100644 --- a/platform/dashboard/src/features/tour/config/steps.tsx +++ b/platform/dashboard/src/features/tour/config/steps.tsx @@ -10,91 +10,17 @@ export const stepDefinitions: Record = {

Welcome to RushDB!

- Congratulations on creating your account and first workspace. Let’s walk through the key features. + Congratulations on creating your account. We've already created your first project and API key — + let's walk through the key features.

), data: { route: 'home', + redirectTo: 'projectHelp', key: 'welcome' } }, - homeNewProject: { - target: '[data-tour="new-project-btn"]', - placement: 'bottom', - content: ( -
-

Create a Project

-

Projects help you separate and manage your data.

-

- Each project acts as its own data space — perfect for staging vs. production, or isolated tenants. - Click to create your first one — we’ll guide you from there. See{' '} - - our Quick Tutorial{' '} - - to get started faster. -

-
- ), - data: { - route: 'home', - redirectTo: 'newProject', - key: 'homeNewProject' - } - }, - newProjectName: { - target: '[data-tour="project-name-input"]', - placement: 'right', - content: ( -
-

Enter Project Name

-

Give your project a descriptive name so you can find it later.

-
- ), - data: { - route: 'newProject', - key: 'newProjectName' - } - }, - newProjectCustomDb: { - target: '[data-tour="custom-neo4j-container"]', - placement: 'right', - content: ( -
-

Connect Custom Neo4j

-

- You can attach your own Neo4j instance (Aura or self-hosted) for full data isolation and control - over your infrastructure. -

-
- ), - data: { - route: 'newProject', - key: 'newProjectCustomDb' - } - }, - newProjectCreate: { - target: '[data-tour="create-project-btn"]', - placement: 'right', - content: ( -
-

Create Your Project

-

Everything’s set—click “Create project” to finish the setup.

-
- ), - data: { - route: 'newProject', - key: 'newProjectCreate', - noNext: true, - nextShouldBeManuallySet: true, - waitForManualAction: true - } - }, projectSdkTokenOverview: { target: '[data-tour="project-help-sdk-input"]', placement: 'top', diff --git a/platform/dashboard/src/features/tour/stores/tour.ts b/platform/dashboard/src/features/tour/stores/tour.ts index 3ead4ac4..4a82c3ce 100644 --- a/platform/dashboard/src/features/tour/stores/tour.ts +++ b/platform/dashboard/src/features/tour/stores/tour.ts @@ -5,10 +5,6 @@ import type { TourStepKey } from '../types' export const keys: TourStepKey[] = [ 'welcome', - 'homeNewProject', - 'newProjectName', - 'newProjectCustomDb', - 'newProjectCreate', 'projectSdkTokenOverview', 'projectSdkTokenTabInfo', 'projectImportDataTab', diff --git a/platform/dashboard/src/features/tour/types.ts b/platform/dashboard/src/features/tour/types.ts index 3202ee5a..bcc39de0 100644 --- a/platform/dashboard/src/features/tour/types.ts +++ b/platform/dashboard/src/features/tour/types.ts @@ -1,9 +1,5 @@ export type TourStepKey = | 'welcome' - | 'homeNewProject' - | 'newProjectName' - | 'newProjectCustomDb' - | 'newProjectCreate' | 'projectSdkTokenOverview' | 'projectSdkTokenTabInfo' | 'projectImportDataTab' diff --git a/platform/dashboard/src/pages/project/new.tsx b/platform/dashboard/src/pages/project/new.tsx index 178a7aab..afcd0f26 100644 --- a/platform/dashboard/src/pages/project/new.tsx +++ b/platform/dashboard/src/pages/project/new.tsx @@ -11,18 +11,7 @@ import { useCurrentWorkspaceQuery } from '~/features/workspaces/hooks/useWorkspa import { useWorkspaceProjectsQuery } from '~/features/workspaces/hooks/useWorkspaceQueries' import { usePlatformSettings } from '~/features/auth/hooks/useAuthQueries' -import { setTourStep } from '~/features/tour/stores/tour.ts' import { useCallback, useEffect, useMemo, useState } from 'react' -import { useStore } from '@nanostores/react' -import { $tourAllowed, $tourStep } from '~/features/tour/stores/tour.ts' - -const DEFAULT_ONBOARDING_PROJECT_NAME = 'My First RushDB Project' -const onboardingProjectSteps = new Set([ - 'homeNewProject', - 'newProjectName', - 'newProjectCustomDb', - 'newProjectCreate' -]) // Type for form values type ProjectFormValues = { @@ -57,9 +46,6 @@ function CreateProjectForm({ className, ...props }: TPolymorphicComponentProps<' const { data: workspace } = useCurrentWorkspaceQuery() const { data: platformSettings } = usePlatformSettings() const { data: projects, isFetching: isProjectsFetching } = useWorkspaceProjectsQuery() - const tourAllowed = useStore($tourAllowed) - const tourStep = useStore($tourStep) - const isOnboardingProjectCreation = tourAllowed && onboardingProjectSteps.has(tourStep) const maxProjects = workspace?.projectLimit ?? null const showUpgradeButton = useMemo(() => { if (isProjectsFetching) { @@ -73,10 +59,6 @@ function CreateProjectForm({ className, ...props }: TPolymorphicComponentProps<' const [selectedTab, setSelectedTab] = useState<'shared' | 'custom'>('shared') - useEffect(() => { - setTourStep('newProjectName', false) - }, []) - // Use the appropriate schema based on selected tab const getSchema = () => { switch (selectedTab) { @@ -92,7 +74,7 @@ function CreateProjectForm({ className, ...props }: TPolymorphicComponentProps<' const getDefaultValues = useCallback((): Partial => { const base = { description: '', - name: isOnboardingProjectCreation ? DEFAULT_ONBOARDING_PROJECT_NAME : '', + name: '', dataSource: selectedTab as ProjectFormValues['dataSource'] } @@ -102,7 +84,7 @@ function CreateProjectForm({ className, ...props }: TPolymorphicComponentProps<' default: return base } - }, [isOnboardingProjectCreation, selectedTab]) + }, [selectedTab]) const { formState: { errors, isSubmitting }, From 3bb1f3ad19d2f98796dfb841948da9f58204d0ba Mon Sep 17 00:00:00 2001 From: Artemiy Vereshchinskiy Date: Sun, 23 Aug 2026 15:58:19 +0700 Subject: [PATCH 2/2] Add changesets --- .changeset/light-clocks-whisper.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/light-clocks-whisper.md diff --git a/.changeset/light-clocks-whisper.md b/.changeset/light-clocks-whisper.md new file mode 100644 index 00000000..46e06392 --- /dev/null +++ b/.changeset/light-clocks-whisper.md @@ -0,0 +1,6 @@ +--- +'rushdb-dashboard': minor +'rushdb-core': minor +--- + +Minor UX improvements