From 81256a1dc99f411f5a247422e1a9795eaf3e6736 Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Mon, 23 Feb 2026 02:03:45 +0000 Subject: [PATCH] Onboarding --- .../Screens/Dashboard/Home/HomeScreen.tsx | 32 +++++++++++++++---- .../Screens/Dashboard/Home/defaultLayout.tsx | 18 ++--------- src/servers/api/file-browser/router.ts | 16 ++++++++++ .../FileBrowserApp/FileBrowserApp.tsx | 11 +++++-- .../FileBrowserPanelWrapper.tsx | 5 +-- .../FileBrowserApp/components/FileGrid.tsx | 9 ++++-- .../FileBrowserApp/useFileBrowserApp.ts | 16 +++++++--- .../FileViewer/renderers/MarkdownRenderer.tsx | 8 +++++ .../components/Workspace/WorkspaceContext.ts | 7 ++++ .../components/Workspace/WorkspaceView.tsx | 11 +++++-- .../src/components/Workspace/index.ts | 1 + src/workspaces/officerdev/src/index.ts | 2 +- 12 files changed, 100 insertions(+), 36 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Home/HomeScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Home/HomeScreen.tsx index 4dd6e2f7..3e771782 100644 --- a/src/apps/officer-web/Screens/Dashboard/Home/HomeScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Home/HomeScreen.tsx @@ -1,14 +1,34 @@ -import type { LayoutNode } from 'officerdev'; -import { WorkspaceView } from 'officerdev'; +import type { LayoutNode, PanelComponents, DefaultFileSort } from 'officerdev'; +import { WorkspaceView, useFileViewerPanels } from 'officerdev'; import { useWorkspacesState } from 'state/useWorkspacesState'; import { defaultLayout } from './defaultLayout'; -export const HomeScreen = () => { - const workspace = useWorkspacesState('screens/home', defaultLayout); - +const HomeHeader = () => { return ( +
+
+

Welcome to your Officer.dev platform

+

+ Please follow the video instructions below in order to get familiar with all that is possible. +

+
+
+ ); +}; + +const components: PanelComponents = { + 'home-header': HomeHeader, +}; + +const defaultSort: DefaultFileSort = { field: 'type', direction: 'desc' }; + +export const HomeScreen = () => { + const workspace = useWorkspacesState('screens/home', defaultLayout); + const ephemeral = useFileViewerPanels(); + + return (
- +
); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Home/defaultLayout.tsx b/src/apps/officer-web/Screens/Dashboard/Home/defaultLayout.tsx index 967c377c..7e2ef6e6 100644 --- a/src/apps/officer-web/Screens/Dashboard/Home/defaultLayout.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Home/defaultLayout.tsx @@ -3,22 +3,10 @@ import type { LayoutNode } from 'officerdev'; export const defaultLayout: LayoutNode = { type: 'group', id: 'home-root', - direction: 'horizontal', + direction: 'vertical', children: [ - { node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 }, - { - node: { - type: 'group', - id: 'home-center', - direction: 'vertical', - children: [ - { node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 }, - { node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 }, - ], - }, - size: 60, - }, - { node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 }, + { node: { type: 'panel', id: 'home-header', appType: null }, size: 30 }, + { node: { type: 'panel', id: 'home-files', appType: 'officerdev/file-browser' }, size: 70 }, ], }; diff --git a/src/servers/api/file-browser/router.ts b/src/servers/api/file-browser/router.ts index 9e96c167..1d115059 100644 --- a/src/servers/api/file-browser/router.ts +++ b/src/servers/api/file-browser/router.ts @@ -7,6 +7,21 @@ import { getHomeDir, DATA_PATH, getUserSettingsFile } from '@@/data-path'; import * as errors from '@@/custom-errors'; import { readConfig } from '@@/api/server-settings/resources'; +const DEFAULT_HOME_DIRS = ['Downloads', 'Documents', 'Music', 'Video', 'Pictures', 'Onboarding']; +const ONBOARDING_SEED = join(DATA_PATH, 'Onboarding'); + +async function seedHomeDir(homeDir: string) { + for (const dir of DEFAULT_HOME_DIRS) { + const target = join(homeDir, dir); + if (existsSync(target)) continue; + if (dir === 'Onboarding' && existsSync(ONBOARDING_SEED)) { + await cp(ONBOARDING_SEED, target, { recursive: true }); + } else { + await mkdir(target, { recursive: true }); + } + } +} + export const router = createRouter(); type UserCtx = { email: string; role: string | null }; @@ -39,6 +54,7 @@ router.get('/ls', async (ctx) => { // Auto-create dir if missing (only for user home root) if (!ctx.req.query('root') || ctx.req.query('root') === 'home') { + await seedHomeDir(rootDir); await mkdir(absPath, { recursive: true }); } diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx index 7cebabff..3bd553de 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx @@ -7,13 +7,20 @@ import { TaskRunnerDialog } from './components/TaskRunnerDialog'; import { VideoDownloadDialog } from './components/VideoDownloadDialog'; import { useFileBrowserApp } from './useFileBrowserApp'; +type DefaultSort = { + field: 'name' | 'size' | 'type' | 'date'; + direction: 'asc' | 'desc'; +}; + type FileBrowserAppProps = { basePath?: string; rootOverride?: string; + initialPath?: string; + defaultSort?: DefaultSort; }; -export const FileBrowserApp = ({ basePath = '/', rootOverride }: FileBrowserAppProps) => { - const fileBrowserManager = useFileBrowserApp(basePath, rootOverride); +export const FileBrowserApp = ({ basePath = '/', rootOverride, initialPath, defaultSort }: FileBrowserAppProps) => { + const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, initialPath, defaultSort); const { handleNavigate } = fileBrowserManager; return ( diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx index bc95236c..2f7c7b7d 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx @@ -4,7 +4,8 @@ import { FileBrowserApp } from './FileBrowserApp'; const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1)); export const FileBrowserPanelWrapper = () => { - const { cwd } = useWorkspace(); + const { cwd, initialFilePath, defaultFileSort } = useWorkspace(); const basePath = cwdToPath(cwd); - return ; + const initialPath = basePath === '/' ? initialFilePath : undefined; + return ; }; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx index b9c82540..a9484e89 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileGrid.tsx @@ -69,9 +69,10 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => { handleCreateWorkspace, fileScrollRef: scrollRef, } = fileBrowserManager; + const { defaultSort } = fileBrowserManager; const lastClickedIdx = useRef(-1); - const [sortField, setSortField] = useState('name'); - const [sortDirection, setSortDirection] = useState('asc'); + const [sortField, setSortField] = useState(defaultSort?.field ?? 'name'); + const [sortDirection, setSortDirection] = useState(defaultSort?.direction ?? 'asc'); const sorted = (() => { const compare = (a: DirEntry, b: DirEntry): number => { @@ -83,16 +84,18 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => { break; case 'size': result = a.size - b.size; + if (result === 0) return a.name.localeCompare(b.name); break; case 'type': { const typeA = a.type === 'directory' ? 'directory' : getFileType(a.name); const typeB = b.type === 'directory' ? 'directory' : getFileType(b.name); result = typeA.localeCompare(typeB); - if (result === 0) result = a.name.localeCompare(b.name); + if (result === 0) return a.name.localeCompare(b.name); break; } case 'date': result = a.modifiedAt - b.modifiedAt; + if (result === 0) return a.name.localeCompare(b.name); break; } return sortDirection === 'asc' ? result : -result; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts index 1dfa5c11..34533db6 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/useFileBrowserApp.ts @@ -6,16 +6,22 @@ import { useTasks, type TaskSummary } from '../useTasks'; import { useUserState } from 'state/useUserState'; import { useAuth } from 'hooks/useAuth'; -export const useFileBrowserApp = (basePath: string, rootOverride?: string) => { +type DefaultSort = { + field: 'name' | 'size' | 'type' | 'date'; + direction: 'asc' | 'desc'; +}; + +export const useFileBrowserApp = (basePath: string, rootOverride?: string, initialPath?: string, defaultSort?: DefaultSort) => { const { user } = useAuth(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [homeRoot, setHomeRoot] = useUserState('files/homeRoot', 'home'); const [globalPath, setGlobalPath] = useUserState('files/currentPath', '/'); - const [localPath, setLocalPath] = useState(basePath); + const [localPath, setLocalPath] = useState(initialPath ?? basePath); const scoped = basePath !== '/'; - const currentPath = scoped ? localPath : globalPath; - const setCurrentPath = scoped ? setLocalPath : setGlobalPath; + const isolated = !!initialPath; + const currentPath = (scoped || isolated) ? localPath : globalPath; + const setCurrentPath = (scoped || isolated) ? setLocalPath : setGlobalPath; const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useUserState<'grid' | 'list'>('files/viewMode', 'grid'); @@ -620,7 +626,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string) => { // Directory listing visibleEntries, loading, refresh, // View - viewMode, setViewMode, + viewMode, setViewMode, defaultSort, showHidden, setShowHidden, // Selection selected, setSelected, diff --git a/src/workspaces/officerdev/src/apps/FileViewer/renderers/MarkdownRenderer.tsx b/src/workspaces/officerdev/src/apps/FileViewer/renderers/MarkdownRenderer.tsx index 07f89a5a..730a9b16 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/renderers/MarkdownRenderer.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/renderers/MarkdownRenderer.tsx @@ -27,6 +27,14 @@ export const MarkdownRenderer = ({ content, scrollContainer }: MarkdownRendererP remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, rehypeSlug]} components={{ + a({ href, children, ...props }) { + const isExternal = href && !href.startsWith('#'); + return ( + + {children} + + ); + }, pre({ children }) { return
{children}
; }, diff --git a/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts b/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts index 231e5985..eda36b66 100644 --- a/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts +++ b/src/workspaces/officerdev/src/components/Workspace/WorkspaceContext.ts @@ -2,10 +2,17 @@ import { createContext, useContext } from 'react'; import type { DropPosition } from './layout-utils'; +export type DefaultFileSort = { + field: 'name' | 'size' | 'type' | 'date'; + direction: 'asc' | 'desc'; +}; + type WorkspaceContextValue = { workspaceId: string | null; cwd: string; root?: string; + initialFilePath?: string; + defaultFileSort?: DefaultFileSort; swapSourceId: string | null; setSwapSourceId: (id: string | null) => void; onSwap: (sourceId: string, targetId: string) => void; diff --git a/src/workspaces/officerdev/src/components/Workspace/WorkspaceView.tsx b/src/workspaces/officerdev/src/components/Workspace/WorkspaceView.tsx index e1063e8a..e9cbccb2 100644 --- a/src/workspaces/officerdev/src/components/Workspace/WorkspaceView.tsx +++ b/src/workspaces/officerdev/src/components/Workspace/WorkspaceView.tsx @@ -1,7 +1,8 @@ import { useState, useCallback, useEffect } from 'react'; import { flushSync } from 'react-dom'; import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable'; -import type { LayoutNode, WorkspaceState, EphemeralPanels } from './types'; +import type { LayoutNode, WorkspaceState, EphemeralPanels, PanelComponents } from './types'; +import type { DefaultFileSort } from './WorkspaceContext'; import type { DropPosition } from './layout-utils'; import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils'; import { WorkspaceProvider } from './WorkspaceContext'; @@ -12,12 +13,15 @@ type WorkspaceViewProps = { workspace: WorkspaceState; cwd?: string; root?: string; + initialFilePath?: string; + defaultFileSort?: DefaultFileSort; + components?: PanelComponents; ephemeral?: EphemeralPanels | null; }; const noop = () => {}; -export const WorkspaceView = ({ workspace, cwd = '~', root, ephemeral }: WorkspaceViewProps) => { +export const WorkspaceView = ({ workspace, cwd = '~', root, initialFilePath, defaultFileSort, components, ephemeral }: WorkspaceViewProps) => { const { registry } = useAppRegistry(); const layout = workspace.value; @@ -113,6 +117,7 @@ export const WorkspaceView = ({ workspace, cwd = '~', root, ephemeral }: Workspa