build errors
Some checks failed
Deploy Nyla Frontend to Integration / build-and-deploy (push) Failing after 8s

This commit is contained in:
Ida 2026-05-20 18:05:08 +02:00
parent f617a2d701
commit 9488a7d95c
6 changed files with 78 additions and 73 deletions

View file

@ -23,7 +23,6 @@ import {
HiOutlineArrowUturnRight, HiOutlineArrowUturnRight,
HiOutlineTrash, HiOutlineTrash,
HiOutlineDocumentDuplicate, HiOutlineDocumentDuplicate,
HiOutlineArrowLongRight,
HiOutlineChatBubbleLeftEllipsis, HiOutlineChatBubbleLeftEllipsis,
HiOutlineSquares2X2, HiOutlineSquares2X2,
} from 'react-icons/hi2'; } from 'react-icons/hi2';

View file

@ -754,7 +754,10 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
onChange={(e) => { onChange={(e) => {
const typeId = e.target.value; const typeId = e.target.value;
const nextFields = [...(Array.isArray(f.fields) ? f.fields : [])]; 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)) { if (formFieldTypeHasConfigurableOptions(typeId)) {
subRow.options = normalizeFormFieldOptions(subRow.options); subRow.options = normalizeFormFieldOptions(subRow.options);
} }

View file

@ -33,19 +33,6 @@ const _SCOPE_EMOJIS: Record<string, string> = {
const _NEUTRALIZE_ON_EMOJI = '\uD83D\uDD12'; // closed padlock const _NEUTRALIZE_ON_EMOJI = '\uD83D\uDD12'; // closed padlock
const _NEUTRALIZE_OFF_EMOJI = '\uD83D\uDD13'; // open 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. */ /** Internal action keys reserved by the tree for the built-in flag buttons. */
const _ACTION_SCOPE = '__scope__'; const _ACTION_SCOPE = '__scope__';
@ -209,8 +196,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
isDragging, isDragging,
ownership, ownership,
compact, compact,
selectable,
pendingActions,
provider, provider,
onToggleExpand, onToggleExpand,
onToggleSelect, onToggleSelect,
@ -223,9 +208,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
onSendToChat, onSendToChat,
onCycleScope, onCycleScope,
onToggleNeutralize, onToggleNeutralize,
onToggleRagIndex,
onCreateChild,
onExtraAction,
onDragStart, onDragStart,
onDragOver, onDragOver,
onDragLeave, onDragLeave,
@ -294,12 +276,6 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
const canDelete = isOwn && provider.canDelete?.(node); const canDelete = isOwn && provider.canDelete?.(node);
const canPatchScope = isOwn && provider.canPatchScope?.(node); const canPatchScope = isOwn && provider.canPatchScope?.(node);
const canPatchNeutralize = isOwn && provider.canPatchNeutralize?.(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 = [ const rowClasses = [
styles.nodeRow, styles.nodeRow,
@ -473,7 +449,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
tabIndex={-1} tabIndex={-1}
style={{ opacity: node.neutralize ? 1 : 0.35 }} style={{ opacity: node.neutralize ? 1 : 0.35 }}
> >
{_NEUTRALIZE_EMOJI} {node.neutralize ? _NEUTRALIZE_ON_EMOJI : _NEUTRALIZE_OFF_EMOJI}
</button> </button>
)} )}
</div> </div>
@ -520,6 +496,9 @@ export function FormGeneratorTree<T = any>({
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
/** Folders we expanded and confirmed have no visible children → hide chevron like a real leaf */ /** Folders we expanded and confirmed have no visible children → hide chevron like a real leaf */
const [confirmedEmptyFolderIds, setConfirmedEmptyFolderIds] = useState(() => new Set<string>()); 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 lastSelectedIdRef = useRef<string | null>(null);
const treeContentRef = useRef<HTMLDivElement>(null); const treeContentRef = useRef<HTMLDivElement>(null);
/** Tracks node ids for which auto-expand has already fired (one-shot). */ /** Tracks node ids for which auto-expand has already fired (one-shot). */
@ -667,25 +646,11 @@ export function FormGeneratorTree<T = any>({
const _handleToggleExpand = useCallback( const _handleToggleExpand = useCallback(
async (id: string) => { async (id: string) => {
const wasExpanded = expandedIds.has(id); const wasExpanded = expandedIds.has(id);
const node = nodes.find((n) => n.id === id); const node = nodes.find((n) => n.id === id);
if (node && !wasExpanded) {
const childMap = _buildChildMap(nodes); if (wasExpanded) {
const existingChildren = childMap.get(id); // Collapse: remove all descendants from nodes state and expandedIds.
if (!existingChildren || existingChildren.length === 0) { const descendantIds = new Set(_collectDescendantIds(id, nodes));
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);
setExpandedIds((prev) => { setExpandedIds((prev) => {
const next = new Set(prev); const next = new Set(prev);
next.delete(id); next.delete(id);
@ -693,17 +658,30 @@ export function FormGeneratorTree<T = any>({
return next; return next;
}); });
setNodes((prev) => prev.filter((n) => !descendantIds.has(n.id))); setNodes((prev) => prev.filter((n) => !descendantIds.has(n.id)));
} else { return;
// Expand: load children from backend (always fresh). }
setExpandedIds((prev) => new Set([...prev, id]));
// 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); const childNodes = await provider.loadChildren(id, ownership);
if (childNodes.length > 0) { if (childNodes.length > 0) {
setNodes((prev) => _mergeNodes(prev, childNodes)); 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], [nodes, expandedIds, provider, ownership, _mergeNodes],
); );
@ -818,24 +796,18 @@ export function FormGeneratorTree<T = any>({
if (!trimmed) return; if (!trimmed) return;
try { try {
const newNode = await provider.createChild(parentId, trimmed); const newNode = await provider.createChild(parentId, trimmed);
<<<<<<< HEAD
setNodes((prev) => _mergeNodes(prev, [newNode])); setNodes((prev) => _mergeNodes(prev, [newNode]));
// The provider may have re-parented `newNode` (e.g. onto a synth-root) // The provider may have re-parented `newNode` (e.g. onto a synth-root)
// when `parentId === null`; expand whichever parent the resulting node // when `parentId === null`; expand whichever parent the resulting node
// actually points at, so the new folder is visible. // actually points at, so the new folder is visible.
const visibleParent = newNode.parentId ?? null; const visibleParent = newNode.parentId ?? null;
if (visibleParent) { if (visibleParent) {
setExpandedIds((prev) => new Set(prev).add(visibleParent));
=======
setNodes((prev) => [...prev, newNode]);
if (parentId) {
setConfirmedEmptyFolderIds((prev) => { setConfirmedEmptyFolderIds((prev) => {
const next = new Set(prev); const next = new Set(prev);
next.delete(parentId); next.delete(visibleParent);
return next; return next;
}); });
setExpandedIds((prev) => new Set(prev).add(parentId)); setExpandedIds((prev) => new Set(prev).add(visibleParent));
>>>>>>> ae63020 (finished file tree folder selection in file create node)
} }
} catch { } catch {
await _handleRefresh(); await _handleRefresh();
@ -1203,11 +1175,7 @@ export function FormGeneratorTree<T = any>({
</div> </div>
)} )}
<<<<<<< HEAD {selectable && selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
{selectable && selectedIds.size > 0 && batchActions.length > 0 && (
=======
{selectedIds.size > 0 && batchActions.length > 0 && !hideRowActionButtons && (
>>>>>>> 7fb9645 (workign on folder location in file create node)
<div className={styles.batchToolbar}> <div className={styles.batchToolbar}>
<span className={styles.batchCount}>{selectedIds.size} selected</span> <span className={styles.batchCount}>{selectedIds.size} selected</span>
{batchActions.map((action: TreeBatchAction) => { {batchActions.map((action: TreeBatchAction) => {

View file

@ -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 { export function createFolderFileProvider(options: { includeFiles?: boolean } = {}): TreeNodeProvider {
const includeFiles = options.includeFiles !== false; const includeFiles = options.includeFiles !== false;
const ownerParam = (ownership: Ownership) => (ownership === 'own' ? 'me' : 'shared'); 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 foldersRes = await api.get('/api/files/folders/tree', { params: { owner } });
const allFolders: FolderData[] = foldersRes.data ?? []; const allFolders: FolderData[] = foldersRes.data ?? [];
const childFolders = allFolders.filter((f) => (f.parentId ?? null) === parentId); const childFolders = allFolders.filter((f) => (f.parentId ?? null) === apiParentId);
nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles))); 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) { if (includeFiles) {
try { try {
const filters: Record<string, any> = {}; const filters: Record<string, any> = {};
if (parentId) { if (apiParentId) {
filters.folderId = parentId; filters.folderId = apiParentId;
} }
const paginationParam = JSON.stringify({ filters, pageSize: 500 }); const paginationParam = JSON.stringify({ filters, pageSize: 500 });
const filesRes = await api.get('/api/files/list', { const filesRes = await api.get('/api/files/list', {
@ -147,12 +178,16 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
rawFiles = data; rawFiles = data;
} }
let matched = rawFiles.filter((f) => (f.folderId ?? null) === parentId); let matched = rawFiles.filter((f) => (f.folderId ?? null) === apiParentId);
if (ownership === 'shared') { if (ownership === 'shared') {
const myId = getUserDataCache()?.id; const myId = getUserDataCache()?.id;
if (myId) matched = matched.filter((f) => f.sysCreatedBy !== myId); 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 { } catch {
// file list may fail for shared trees; folders still render // file list may fail for shared trees; folders still render
} }
@ -225,8 +260,8 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
: targetParentId; : targetParentId;
await Promise.all( await Promise.all(
ids.map((id) => { ids.map((id) => {
if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: targetParentId }); if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: apiTarget });
return api.post(`/api/files/folders/${id}/move`, { parentId: targetParentId }); return api.post(`/api/files/folders/${id}/move`, { parentId: apiTarget });
}), }),
); );
}, },

View file

@ -10,6 +10,7 @@ import { Outlet, useLocation } from 'react-router-dom';
import { FeatureProvider, useFeatureStore } from '../stores/featureStore'; import { FeatureProvider, useFeatureStore } from '../stores/featureStore';
import { MandateNavigation } from '../components/Navigation/MandateNavigation'; import { MandateNavigation } from '../components/Navigation/MandateNavigation';
import { UserSection } from '../components/Navigation/UserSection'; import { UserSection } from '../components/Navigation/UserSection';
import { RagRunningBadge } from '../components/RagRunningBadge/RagRunningBadge';
import { KEEP_ALIVE_ROUTES, hideFeatureOutlet } from '../config/keepAliveRoutes'; import { KEEP_ALIVE_ROUTES, hideFeatureOutlet } from '../config/keepAliveRoutes';
import type { KeepAliveEntry, KeepAliveScopedEntry, KeepAliveUnscopedEntry } from '../types/keepAlive.types'; import type { KeepAliveEntry, KeepAliveScopedEntry, KeepAliveUnscopedEntry } from '../types/keepAlive.types';
import { isKeepAliveScoped } from '../types/keepAlive.types'; import { isKeepAliveScoped } from '../types/keepAlive.types';

View file

@ -559,7 +559,6 @@ const TaskCard: React.FC<TaskCardProps> = ({
dismissing = false, dismissing = false,
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { request } = useApiRequest();
const { handleFileUpload } = useFileOperations(); const { handleFileUpload } = useFileOperations();
const [formData, setFormData] = useState<Record<string, unknown>>({}); const [formData, setFormData] = useState<Record<string, unknown>>({});
const [formPopupOpen, setFormPopupOpen] = useState(false); const [formPopupOpen, setFormPopupOpen] = useState(false);