Onboarding
This commit is contained in:
@@ -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 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+3
-2
@@ -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} />;
|
||||
};
|
||||
|
||||
+6
-3
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user