Onboarding
This commit is contained in:
@@ -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',
|
direction: 'vertical',
|
||||||
children: [
|
children: [
|
||||||
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-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 },
|
||||||
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 },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
+3
-2
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-3
@@ -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';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user