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,
|
HiOutlineArrowUturnRight,
|
||||||
HiOutlineTrash,
|
HiOutlineTrash,
|
||||||
HiOutlineDocumentDuplicate,
|
HiOutlineDocumentDuplicate,
|
||||||
HiOutlineArrowLongRight,
|
|
||||||
HiOutlineChatBubbleLeftEllipsis,
|
HiOutlineChatBubbleLeftEllipsis,
|
||||||
HiOutlineSquares2X2,
|
HiOutlineSquares2X2,
|
||||||
} from 'react-icons/hi2';
|
} from 'react-icons/hi2';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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).
|
}
|
||||||
|
|
||||||
|
// 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]));
|
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(() => {
|
setTimeout(() => {
|
||||||
_scrollExpandedNodeToCenter(id);
|
_scrollExpandedNodeToCenter(id);
|
||||||
}, 50);
|
}, 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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue