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 { 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<LayoutNode>('screens/home', defaultLayout);
const HomeHeader = () => {
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">
<WorkspaceView workspace={workspace} />
<WorkspaceView workspace={workspace} initialFilePath="/Onboarding" defaultFileSort={defaultSort} components={components} ephemeral={ephemeral} />
</div>
);
};
@@ -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 },
],
};
+16
View File
@@ -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 });
}
@@ -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 (
@@ -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 <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,
fileScrollRef: scrollRef,
} = fileBrowserManager;
const { defaultSort } = fileBrowserManager;
const lastClickedIdx = useRef<number>(-1);
const [sortField, setSortField] = useState<SortField>('name');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const [sortField, setSortField] = useState<SortField>(defaultSort?.field ?? 'name');
const [sortDirection, setSortDirection] = useState<SortDirection>(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;
@@ -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<HomeRoot>('files/homeRoot', 'home');
const [globalPath, setGlobalPath] = useUserState<string>('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<DirEntry[]>([]);
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,
@@ -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 (
<a href={href} {...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})} {...props}>
{children}
</a>
);
},
pre({ children }) {
return <div className="relative">{children}</div>;
},
@@ -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;
@@ -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
<WorkspaceRenderer
layout={layout}
registry={registry}
components={components}
interactive
onSetApp={handleSetApp}
onSplit={handleSplit}
@@ -149,6 +154,8 @@ export const WorkspaceView = ({ workspace, cwd = '~', root, ephemeral }: Workspa
workspaceId: workspace.key,
cwd,
root,
initialFilePath,
defaultFileSort,
swapSourceId,
setSwapSourceId,
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 { DropPosition } 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 { WorkspaceView } from './WorkspaceView';
export { WorkspaceLayout } from './WorkspaceLayout';
+1 -1
View File
@@ -20,4 +20,4 @@ export { ProjectListApp, ProjectPreview, SELECTED_PROJECT, CREATING_PROJECT, EDI
// Workspace
export { WorkspaceView, WorkspaceLayout, WorkspaceProvider, useWorkspace } 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';