Onboarding

This commit is contained in:
2026-02-23 02:03:45 +00:00
parent 97da2e2736
commit 81256a1dc9
12 changed files with 100 additions and 36 deletions
@@ -1,14 +1,34 @@
import type { LayoutNode } from 'officerdev'; import type { LayoutNode, PanelComponents, DefaultFileSort } from 'officerdev';
import { WorkspaceView } from 'officerdev'; import { WorkspaceView, useFileViewerPanels } from 'officerdev';
import { useWorkspacesState } from 'state/useWorkspacesState'; import { useWorkspacesState } from 'state/useWorkspacesState';
import { defaultLayout } from './defaultLayout'; import { defaultLayout } from './defaultLayout';
export const HomeScreen = () => { const HomeHeader = () => {
const workspace = useWorkspacesState<LayoutNode>('screens/home', defaultLayout);
return ( return (
<div className="flex h-full items-center justify-center p-6 text-center">
<div>
<h1 className="text-2xl font-bold">Welcome to your Officer.dev platform</h1>
<p className="mt-2 text-sm opacity-70">
Please follow the video instructions below in order to get familiar with all that is possible.
</p>
</div>
</div>
);
};
const components: PanelComponents = {
'home-header': HomeHeader,
};
const defaultSort: DefaultFileSort = { field: 'type', direction: 'desc' };
export const HomeScreen = () => {
const workspace = useWorkspacesState<LayoutNode>('screens/home', defaultLayout);
const ephemeral = useFileViewerPanels();
return (
<div className="h-full w-full"> <div className="h-full w-full">
<WorkspaceView workspace={workspace} /> <WorkspaceView workspace={workspace} initialFilePath="/Onboarding" defaultFileSort={defaultSort} components={components} ephemeral={ephemeral} />
</div> </div>
); );
}; };
@@ -3,22 +3,10 @@ import type { LayoutNode } from 'officerdev';
export const defaultLayout: LayoutNode = { export const defaultLayout: LayoutNode = {
type: 'group', type: 'group',
id: 'home-root', id: 'home-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
{
node: {
type: 'group',
id: 'home-center',
direction: 'vertical', direction: 'vertical',
children: [ children: [
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 }, { node: { type: 'panel', id: 'home-header', appType: null }, size: 30 },
{ node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 }, { node: { type: 'panel', id: 'home-files', appType: 'officerdev/file-browser' }, size: 70 },
],
},
size: 60,
},
{ node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 },
], ],
}; };
+16
View File
@@ -7,6 +7,21 @@ import { getHomeDir, DATA_PATH, getUserSettingsFile } from '@@/data-path';
import * as errors from '@@/custom-errors'; import * as errors from '@@/custom-errors';
import { readConfig } from '@@/api/server-settings/resources'; 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(); export const router = createRouter();
type UserCtx = { email: string; role: string | null }; 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) // Auto-create dir if missing (only for user home root)
if (!ctx.req.query('root') || ctx.req.query('root') === 'home') { if (!ctx.req.query('root') || ctx.req.query('root') === 'home') {
await seedHomeDir(rootDir);
await mkdir(absPath, { recursive: true }); await mkdir(absPath, { recursive: true });
} }
@@ -7,13 +7,20 @@ import { TaskRunnerDialog } from './components/TaskRunnerDialog';
import { VideoDownloadDialog } from './components/VideoDownloadDialog'; import { VideoDownloadDialog } from './components/VideoDownloadDialog';
import { useFileBrowserApp } from './useFileBrowserApp'; import { useFileBrowserApp } from './useFileBrowserApp';
type DefaultSort = {
field: 'name' | 'size' | 'type' | 'date';
direction: 'asc' | 'desc';
};
type FileBrowserAppProps = { type FileBrowserAppProps = {
basePath?: string; basePath?: string;
rootOverride?: string; rootOverride?: string;
initialPath?: string;
defaultSort?: DefaultSort;
}; };
export const FileBrowserApp = ({ basePath = '/', rootOverride }: FileBrowserAppProps) => { export const FileBrowserApp = ({ basePath = '/', rootOverride, initialPath, defaultSort }: FileBrowserAppProps) => {
const fileBrowserManager = useFileBrowserApp(basePath, rootOverride); const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, initialPath, defaultSort);
const { handleNavigate } = fileBrowserManager; const { handleNavigate } = fileBrowserManager;
return ( return (
@@ -4,7 +4,8 @@ import { FileBrowserApp } from './FileBrowserApp';
const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1)); const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
export const FileBrowserPanelWrapper = () => { export const FileBrowserPanelWrapper = () => {
const { cwd } = useWorkspace(); const { cwd, initialFilePath, defaultFileSort } = useWorkspace();
const basePath = cwdToPath(cwd); const basePath = cwdToPath(cwd);
return <FileBrowserApp basePath={basePath} rootOverride={basePath !== '/' ? 'home' : undefined} />; const initialPath = basePath === '/' ? initialFilePath : undefined;
return <FileBrowserApp basePath={basePath} rootOverride={basePath !== '/' ? 'home' : undefined} initialPath={initialPath} defaultSort={defaultFileSort} />;
}; };
@@ -69,9 +69,10 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
handleCreateWorkspace, handleCreateWorkspace,
fileScrollRef: scrollRef, fileScrollRef: scrollRef,
} = fileBrowserManager; } = fileBrowserManager;
const { defaultSort } = fileBrowserManager;
const lastClickedIdx = useRef<number>(-1); const lastClickedIdx = useRef<number>(-1);
const [sortField, setSortField] = useState<SortField>('name'); const [sortField, setSortField] = useState<SortField>(defaultSort?.field ?? 'name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc'); const [sortDirection, setSortDirection] = useState<SortDirection>(defaultSort?.direction ?? 'asc');
const sorted = (() => { const sorted = (() => {
const compare = (a: DirEntry, b: DirEntry): number => { const compare = (a: DirEntry, b: DirEntry): number => {
@@ -83,16 +84,18 @@ export const FileGrid = ({ fileBrowserManager }: FileGridProps) => {
break; break;
case 'size': case 'size':
result = a.size - b.size; result = a.size - b.size;
if (result === 0) return a.name.localeCompare(b.name);
break; break;
case 'type': { case 'type': {
const typeA = a.type === 'directory' ? 'directory' : getFileType(a.name); const typeA = a.type === 'directory' ? 'directory' : getFileType(a.name);
const typeB = b.type === 'directory' ? 'directory' : getFileType(b.name); const typeB = b.type === 'directory' ? 'directory' : getFileType(b.name);
result = typeA.localeCompare(typeB); result = typeA.localeCompare(typeB);
if (result === 0) result = a.name.localeCompare(b.name); if (result === 0) return a.name.localeCompare(b.name);
break; break;
} }
case 'date': case 'date':
result = a.modifiedAt - b.modifiedAt; result = a.modifiedAt - b.modifiedAt;
if (result === 0) return a.name.localeCompare(b.name);
break; break;
} }
return sortDirection === 'asc' ? result : -result; return sortDirection === 'asc' ? result : -result;
@@ -6,16 +6,22 @@ import { useTasks, type TaskSummary } from '../useTasks';
import { useUserState } from 'state/useUserState'; import { useUserState } from 'state/useUserState';
import { useAuth } from 'hooks/useAuth'; 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 { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [homeRoot, setHomeRoot] = useUserState<HomeRoot>('files/homeRoot', 'home'); const [homeRoot, setHomeRoot] = useUserState<HomeRoot>('files/homeRoot', 'home');
const [globalPath, setGlobalPath] = useUserState<string>('files/currentPath', '/'); const [globalPath, setGlobalPath] = useUserState<string>('files/currentPath', '/');
const [localPath, setLocalPath] = useState(basePath); const [localPath, setLocalPath] = useState(initialPath ?? basePath);
const scoped = basePath !== '/'; const scoped = basePath !== '/';
const currentPath = scoped ? localPath : globalPath; const isolated = !!initialPath;
const setCurrentPath = scoped ? setLocalPath : setGlobalPath; const currentPath = (scoped || isolated) ? localPath : globalPath;
const setCurrentPath = (scoped || isolated) ? setLocalPath : setGlobalPath;
const [entries, setEntries] = useState<DirEntry[]>([]); const [entries, setEntries] = useState<DirEntry[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useUserState<'grid' | 'list'>('files/viewMode', 'grid'); const [viewMode, setViewMode] = useUserState<'grid' | 'list'>('files/viewMode', 'grid');
@@ -620,7 +626,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string) => {
// Directory listing // Directory listing
visibleEntries, loading, refresh, visibleEntries, loading, refresh,
// View // View
viewMode, setViewMode, viewMode, setViewMode, defaultSort,
showHidden, setShowHidden, showHidden, setShowHidden,
// Selection // Selection
selected, setSelected, selected, setSelected,
@@ -27,6 +27,14 @@ export const MarkdownRenderer = ({ content, scrollContainer }: MarkdownRendererP
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSlug]} rehypePlugins={[rehypeRaw, rehypeSlug]}
components={{ components={{
a({ href, children, ...props }) {
const isExternal = href && !href.startsWith('#');
return (
<a href={href} {...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})} {...props}>
{children}
</a>
);
},
pre({ children }) { pre({ children }) {
return <div className="relative">{children}</div>; return <div className="relative">{children}</div>;
}, },
@@ -2,10 +2,17 @@ import { createContext, useContext } from 'react';
import type { DropPosition } from './layout-utils'; import type { DropPosition } from './layout-utils';
export type DefaultFileSort = {
field: 'name' | 'size' | 'type' | 'date';
direction: 'asc' | 'desc';
};
type WorkspaceContextValue = { type WorkspaceContextValue = {
workspaceId: string | null; workspaceId: string | null;
cwd: string; cwd: string;
root?: string; root?: string;
initialFilePath?: string;
defaultFileSort?: DefaultFileSort;
swapSourceId: string | null; swapSourceId: string | null;
setSwapSourceId: (id: string | null) => void; setSwapSourceId: (id: string | null) => void;
onSwap: (sourceId: string, targetId: string) => void; onSwap: (sourceId: string, targetId: string) => void;
@@ -1,7 +1,8 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { flushSync } from 'react-dom'; import { flushSync } from 'react-dom';
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable'; 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 type { DropPosition } from './layout-utils';
import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils'; import { splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, countPanels } from './layout-utils';
import { WorkspaceProvider } from './WorkspaceContext'; import { WorkspaceProvider } from './WorkspaceContext';
@@ -12,12 +13,15 @@ type WorkspaceViewProps = {
workspace: WorkspaceState; workspace: WorkspaceState;
cwd?: string; cwd?: string;
root?: string; root?: string;
initialFilePath?: string;
defaultFileSort?: DefaultFileSort;
components?: PanelComponents;
ephemeral?: EphemeralPanels | null; ephemeral?: EphemeralPanels | null;
}; };
const noop = () => {}; 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 { registry } = useAppRegistry();
const layout = workspace.value; const layout = workspace.value;
@@ -113,6 +117,7 @@ export const WorkspaceView = ({ workspace, cwd = '~', root, ephemeral }: Workspa
<WorkspaceRenderer <WorkspaceRenderer
layout={layout} layout={layout}
registry={registry} registry={registry}
components={components}
interactive interactive
onSetApp={handleSetApp} onSetApp={handleSetApp}
onSplit={handleSplit} onSplit={handleSplit}
@@ -149,6 +154,8 @@ export const WorkspaceView = ({ workspace, cwd = '~', root, ephemeral }: Workspa
workspaceId: workspace.key, workspaceId: workspace.key,
cwd, cwd,
root, root,
initialFilePath,
defaultFileSort,
swapSourceId, swapSourceId,
setSwapSourceId, setSwapSourceId,
onSwap: handleSwap, onSwap: handleSwap,
@@ -1,6 +1,7 @@
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, HomeRoot } from './types'; export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, HomeRoot } from './types';
export type { DropPosition } from './layout-utils'; export type { DropPosition } from './layout-utils';
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils'; export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './layout-utils';
export type { DefaultFileSort } from './WorkspaceContext';
export { WorkspaceProvider, useWorkspace } from './WorkspaceContext'; export { WorkspaceProvider, useWorkspace } from './WorkspaceContext';
export { WorkspaceView } from './WorkspaceView'; export { WorkspaceView } from './WorkspaceView';
export { WorkspaceLayout } from './WorkspaceLayout'; export { WorkspaceLayout } from './WorkspaceLayout';
+1 -1
View File
@@ -20,4 +20,4 @@ export { ProjectListApp, ProjectPreview, SELECTED_PROJECT, CREATING_PROJECT, EDI
// Workspace // Workspace
export { WorkspaceView, WorkspaceLayout, WorkspaceProvider, useWorkspace } from './components/Workspace'; export { WorkspaceView, WorkspaceLayout, WorkspaceProvider, useWorkspace } from './components/Workspace';
export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './components/Workspace'; export { createDefaultLayout, splitPanel, removePanel, setApp, updateSizes, swapPanels, movePanel, pruneEmptyPanels, countPanels, hasAnyApp } from './components/Workspace';
export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, DropPosition, HomeRoot } from './components/Workspace'; export type { LayoutNode, LayoutGroup, LayoutPanel, WorkspaceDefinition, WorkspaceState, ProjectType, ProjectDefinition, AppRegistry, AppRegistryEntry, PanelComponents, PanelComponentEntry, EphemeralPanels, DropPosition, HomeRoot, DefaultFileSort } from './components/Workspace';