build errors
Some checks failed
Deploy Nyla Frontend to Integration / build-and-deploy (push) Failing after 8s
Some checks failed
Deploy Nyla Frontend to Integration / build-and-deploy (push) Failing after 8s
This commit is contained in:
parent
f617a2d701
commit
9488a7d95c
6 changed files with 78 additions and 73 deletions
|
|
@ -23,7 +23,6 @@ import {
|
|||
HiOutlineArrowUturnRight,
|
||||
HiOutlineTrash,
|
||||
HiOutlineDocumentDuplicate,
|
||||
HiOutlineArrowLongRight,
|
||||
HiOutlineChatBubbleLeftEllipsis,
|
||||
HiOutlineSquares2X2,
|
||||
} from 'react-icons/hi2';
|
||||
|
|
|
|||
|
|
@ -754,7 +754,10 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
onChange={(e) => {
|
||||
const typeId = e.target.value;
|
||||
const nextFields = [...(Array.isArray(f.fields) ? f.fields : [])];
|
||||
const subRow = { ...(nextFields[j] as Record<string, unknown>), type: typeId };
|
||||
const subRow: Record<string, unknown> = {
|
||||
...(nextFields[j] as Record<string, unknown>),
|
||||
type: typeId,
|
||||
};
|
||||
if (formFieldTypeHasConfigurableOptions(typeId)) {
|
||||
subRow.options = normalizeFormFieldOptions(subRow.options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,19 +33,6 @@ const _SCOPE_EMOJIS: Record<string, string> = {
|
|||
|
||||
const _NEUTRALIZE_ON_EMOJI = '\uD83D\uDD12'; // closed padlock
|
||||
const _NEUTRALIZE_OFF_EMOJI = '\uD83D\uDD13'; // open padlock
|
||||
const _RAG_ON_EMOJI = '\uD83E\uDDE0'; // brain
|
||||
const _RAG_OFF_EMOJI = '\uD83E\uDDE0'; // brain (greyed via CSS filter when off)
|
||||
|
||||
/** CSS for the OFF-state of a boolean flag button. We desaturate the colour
|
||||
* emoji and dim it so the on/off transition is obvious at a glance, even
|
||||
* when the on/off glyph itself is similar (e.g. brain vs greyed-brain). */
|
||||
const _OFF_STATE_STYLE: React.CSSProperties = {
|
||||
filter: 'grayscale(1)',
|
||||
opacity: 0.45,
|
||||
};
|
||||
|
||||
/** Uniform symbol for any flag whose effective value is 'mixed' across children. */
|
||||
const _MIXED_SYMBOL = '\u25E9';
|
||||
|
||||
/** Internal action keys reserved by the tree for the built-in flag buttons. */
|
||||
const _ACTION_SCOPE = '__scope__';
|
||||
|
|
@ -209,8 +196,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
isDragging,
|
||||
ownership,
|
||||
compact,
|
||||
selectable,
|
||||
pendingActions,
|
||||
provider,
|
||||
onToggleExpand,
|
||||
onToggleSelect,
|
||||
|
|
@ -223,9 +208,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
onSendToChat,
|
||||
onCycleScope,
|
||||
onToggleNeutralize,
|
||||
onToggleRagIndex,
|
||||
onCreateChild,
|
||||
onExtraAction,
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDragLeave,
|
||||
|
|
@ -294,12 +276,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
const canDelete = isOwn && provider.canDelete?.(node);
|
||||
const canPatchScope = isOwn && provider.canPatchScope?.(node);
|
||||
const canPatchNeutralize = isOwn && provider.canPatchNeutralize?.(node);
|
||||
const canPatchRagIndex = isOwn && provider.canPatchRagIndex?.(node);
|
||||
const canCreateChild =
|
||||
isOwn &&
|
||||
!!provider.createChild &&
|
||||
node.type === 'folder' &&
|
||||
(provider.canCreate ? provider.canCreate(node.id) : true);
|
||||
|
||||
const rowClasses = [
|
||||
styles.nodeRow,
|
||||
|
|
@ -473,7 +449,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
tabIndex={-1}
|
||||
style={{ opacity: node.neutralize ? 1 : 0.35 }}
|
||||
>
|
||||
{_NEUTRALIZE_EMOJI}
|
||||
{node.neutralize ? _NEUTRALIZE_ON_EMOJI : _NEUTRALIZE_OFF_EMOJI}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -520,6 +496,9 @@ export function FormGeneratorTree<T = any>({
|
|||
const [filterText, setFilterText] = useState('');
|
||||
/** Folders we expanded and confirmed have no visible children → hide chevron like a real leaf */
|
||||
const [confirmedEmptyFolderIds, setConfirmedEmptyFolderIds] = useState(() => new Set<string>());
|
||||
/** Per-node set of in-flight action keys (e.g. scope/neutralize/rag) so rows
|
||||
* can render a spinner over the corresponding button. */
|
||||
const [pendingActions, setPendingActions] = useState<Map<string, Set<string>>>(() => new Map());
|
||||
const lastSelectedIdRef = useRef<string | null>(null);
|
||||
const treeContentRef = useRef<HTMLDivElement>(null);
|
||||
/** Tracks node ids for which auto-expand has already fired (one-shot). */
|
||||
|
|
@ -667,25 +646,11 @@ export function FormGeneratorTree<T = any>({
|
|||
const _handleToggleExpand = useCallback(
|
||||
async (id: string) => {
|
||||
const wasExpanded = expandedIds.has(id);
|
||||
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
if (node && !wasExpanded) {
|
||||
const childMap = _buildChildMap(nodes);
|
||||
const existingChildren = childMap.get(id);
|
||||
if (!existingChildren || existingChildren.length === 0) {
|
||||
const childNodes = await provider.loadChildren(id, ownership);
|
||||
if (childNodes.length > 0) {
|
||||
setNodes((prev) => [...prev, ...childNodes]);
|
||||
setConfirmedEmptyFolderIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
} else if (node.type === 'folder') {
|
||||
setConfirmedEmptyFolderIds((prev) => new Set(prev).add(id));
|
||||
}
|
||||
};
|
||||
_collectDescendants(id);
|
||||
|
||||
if (wasExpanded) {
|
||||
// Collapse: remove all descendants from nodes state and expandedIds.
|
||||
const descendantIds = new Set(_collectDescendantIds(id, nodes));
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
|
|
@ -693,17 +658,30 @@ export function FormGeneratorTree<T = any>({
|
|||
return next;
|
||||
});
|
||||
setNodes((prev) => prev.filter((n) => !descendantIds.has(n.id)));
|
||||
} else {
|
||||
// Expand: load children from backend (always fresh).
|
||||
setExpandedIds((prev) => new Set([...prev, id]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand: load children from backend (fresh) and track empty folders so
|
||||
// we can hide the chevron for confirmed-empty ones.
|
||||
setExpandedIds((prev) => new Set([...prev, id]));
|
||||
const childMap = _buildChildMap(nodes);
|
||||
const existingChildren = childMap.get(id);
|
||||
if (!existingChildren || existingChildren.length === 0) {
|
||||
const childNodes = await provider.loadChildren(id, ownership);
|
||||
if (childNodes.length > 0) {
|
||||
setNodes((prev) => _mergeNodes(prev, childNodes));
|
||||
setConfirmedEmptyFolderIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
} else if (node?.type === 'folder') {
|
||||
setConfirmedEmptyFolderIds((prev) => new Set(prev).add(id));
|
||||
}
|
||||
setTimeout(() => {
|
||||
_scrollExpandedNodeToCenter(id);
|
||||
}, 50);
|
||||
}
|
||||
setTimeout(() => {
|
||||
_scrollExpandedNodeToCenter(id);
|
||||
}, 50);
|
||||
},
|
||||
[nodes, expandedIds, provider, ownership, _mergeNodes],
|
||||
);
|
||||
|
|
@ -818,24 +796,18 @@ export function FormGeneratorTree<T = any>({
|
|||
if (!trimmed) return;
|
||||
try {
|
||||
const newNode = await provider.createChild(parentId, trimmed);
|
||||
<<<<<<< HEAD
|
||||
setNodes((prev) => _mergeNodes(prev, [newNode]));
|
||||
// The provider may have re-parented `newNode` (e.g. onto a synth-root)
|
||||
// when `parentId === null`; expand whichever parent the resulting node
|
||||
// actually points at, so the new folder is visible.
|
||||
const visibleParent = newNode.parentId ?? null;
|
||||
if (visibleParent) {
|
||||
setExpandedIds((prev) => new Set(prev).add(visibleParent));
|
||||
=======
|
||||
setNodes((prev) => [...prev, newNode]);
|
||||
if (parentId) {
|
||||
setConfirmedEmptyFolderIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(parentId);
|
||||
next.delete(visibleParent);
|
||||
return next;
|
||||
});
|
||||
setExpandedIds((prev) => new Set(prev).add(parentId));
|
||||
>>>>>>> ae63020 (finished file tree folder selection in file create node)
|
||||
setExpandedIds((prev) => new Set(prev).add(visibleParent));
|
||||
}
|
||||
} catch {
|
||||
await _handleRefresh();
|
||||
|
|
@ -1203,11 +1175,7 @@ export function FormGeneratorTree<T = any>({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<<<<<<< HEAD
|
||||
{selectable && selectedIds.size > 0 && batchActions.length > 0 && (
|
||||
=======
|
||||
{selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
|
||||
>>>>>>> 7fb9645 (workign on folder location in file create node)
|
||||
{selectable && selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
|
||||
<div className={styles.batchToolbar}>
|
||||
<span className={styles.batchCount}>{selectedIds.size} selected</span>
|
||||
{batchActions.map((action: TreeBatchAction) => {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,33 @@ function _mapFileToNode(file: FileData, ownership: Ownership): TreeNode {
|
|||
};
|
||||
}
|
||||
|
||||
/** Stable synthetic root id per ownership scope. The real top-level
|
||||
* folders/files attach their `parentId` to this id once we re-parent them
|
||||
* in `loadChildren`. The id stays inside the FE provider; the backend
|
||||
* never sees it. */
|
||||
const _SYNTH_ROOT_ID = (ownership: Ownership): string => `__filesRoot:${ownership}`;
|
||||
|
||||
/** Build the synthetic root node. Its only job is to:
|
||||
* - act as a drop-target for moving items back to top-level,
|
||||
* - expose a global neutralize/scope toggle that cascades to every
|
||||
* top-level descendant.
|
||||
* Its scope/neutralize values are intentionally `undefined` (= "no own
|
||||
* state") — the icons render an indeterminate state and a click sets the
|
||||
* intent on every owned descendant. */
|
||||
function _makeSyntheticRoot(ownership: Ownership): TreeNode {
|
||||
return {
|
||||
id: _SYNTH_ROOT_ID(ownership),
|
||||
name: '/',
|
||||
type: 'folder',
|
||||
parentId: null,
|
||||
ownership,
|
||||
icon: <FaFolder style={{ color: '#666' }} />,
|
||||
defaultExpanded: true,
|
||||
scope: 'personal',
|
||||
neutralize: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFolderFileProvider(options: { includeFiles?: boolean } = {}): TreeNodeProvider {
|
||||
const includeFiles = options.includeFiles !== false;
|
||||
const ownerParam = (ownership: Ownership) => (ownership === 'own' ? 'me' : 'shared');
|
||||
|
|
@ -127,14 +154,18 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
|
||||
const foldersRes = await api.get('/api/files/folders/tree', { params: { owner } });
|
||||
const allFolders: FolderData[] = foldersRes.data ?? [];
|
||||
const childFolders = allFolders.filter((f) => (f.parentId ?? null) === parentId);
|
||||
nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles)));
|
||||
const childFolders = allFolders.filter((f) => (f.parentId ?? null) === apiParentId);
|
||||
const folderNodes = childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles));
|
||||
if (apiParentId === null) {
|
||||
for (const n of folderNodes) n.parentId = synthRootId;
|
||||
}
|
||||
nodes.push(...folderNodes);
|
||||
|
||||
if (includeFiles) {
|
||||
try {
|
||||
const filters: Record<string, any> = {};
|
||||
if (parentId) {
|
||||
filters.folderId = parentId;
|
||||
if (apiParentId) {
|
||||
filters.folderId = apiParentId;
|
||||
}
|
||||
const paginationParam = JSON.stringify({ filters, pageSize: 500 });
|
||||
const filesRes = await api.get('/api/files/list', {
|
||||
|
|
@ -147,12 +178,16 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
} else if (Array.isArray(data)) {
|
||||
rawFiles = data;
|
||||
}
|
||||
let matched = rawFiles.filter((f) => (f.folderId ?? null) === parentId);
|
||||
let matched = rawFiles.filter((f) => (f.folderId ?? null) === apiParentId);
|
||||
if (ownership === 'shared') {
|
||||
const myId = getUserDataCache()?.id;
|
||||
if (myId) matched = matched.filter((f) => f.sysCreatedBy !== myId);
|
||||
}
|
||||
nodes.push(...matched.map((f) => _mapFileToNode(f, ownership)));
|
||||
const fileNodes = matched.map((f) => _mapFileToNode(f, ownership));
|
||||
if (apiParentId === null) {
|
||||
for (const n of fileNodes) n.parentId = synthRootId;
|
||||
}
|
||||
nodes.push(...fileNodes);
|
||||
} catch {
|
||||
// file list may fail for shared trees; folders still render
|
||||
}
|
||||
|
|
@ -225,8 +260,8 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
: targetParentId;
|
||||
await Promise.all(
|
||||
ids.map((id) => {
|
||||
if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: targetParentId });
|
||||
return api.post(`/api/files/folders/${id}/move`, { parentId: targetParentId });
|
||||
if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: apiTarget });
|
||||
return api.post(`/api/files/folders/${id}/move`, { parentId: apiTarget });
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Outlet, useLocation } from 'react-router-dom';
|
|||
import { FeatureProvider, useFeatureStore } from '../stores/featureStore';
|
||||
import { MandateNavigation } from '../components/Navigation/MandateNavigation';
|
||||
import { UserSection } from '../components/Navigation/UserSection';
|
||||
import { RagRunningBadge } from '../components/RagRunningBadge/RagRunningBadge';
|
||||
import { KEEP_ALIVE_ROUTES, hideFeatureOutlet } from '../config/keepAliveRoutes';
|
||||
import type { KeepAliveEntry, KeepAliveScopedEntry, KeepAliveUnscopedEntry } from '../types/keepAlive.types';
|
||||
import { isKeepAliveScoped } from '../types/keepAlive.types';
|
||||
|
|
|
|||
|
|
@ -559,7 +559,6 @@ const TaskCard: React.FC<TaskCardProps> = ({
|
|||
dismissing = false,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const { handleFileUpload } = useFileOperations();
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
const [formPopupOpen, setFormPopupOpen] = useState(false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue