Merge branch 'int' of ssh://git.poweron.swiss:2222/PowerOn/ui-nyla into int
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m23s
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m23s
This commit is contained in:
commit
78457a7d27
7 changed files with 517 additions and 8 deletions
17
src/api/workflowApi.connectionPath.test.ts
Normal file
17
src/api/workflowApi.connectionPath.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/** Mirrors _encodedConnectionId in workflowApi.ts for browse/services URL paths. */
|
||||
function encodedConnectionId(connectionId: string): string {
|
||||
return encodeURIComponent(connectionId);
|
||||
}
|
||||
|
||||
describe('connection path encoding for workflow browse', () => {
|
||||
it('encodes spaces and colons in connection:clickup:username references', () => {
|
||||
const ref = 'connection:clickup:Stephan Schellworth';
|
||||
const segment = encodedConnectionId(ref);
|
||||
expect(segment).toBe('connection%3Aclickup%3AStephan%20Schellworth');
|
||||
const url = `/api/workflows/inst/connections/${segment}/browse`;
|
||||
expect(url).not.toContain(' ');
|
||||
expect(url).toContain('%20');
|
||||
});
|
||||
});
|
||||
|
|
@ -943,13 +943,18 @@ export interface ConnectionService {
|
|||
icon: string;
|
||||
}
|
||||
|
||||
/** Encode connection id/reference for URL path segments (may contain spaces/colons). */
|
||||
function _encodedConnectionId(connectionId: string): string {
|
||||
return encodeURIComponent(connectionId);
|
||||
}
|
||||
|
||||
export async function fetchConnectionServices(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
connectionId: string
|
||||
): Promise<ConnectionService[]> {
|
||||
const data = await request({
|
||||
url: `/api/workflows/${instanceId}/connections/${connectionId}/services`,
|
||||
url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/services`,
|
||||
method: 'get',
|
||||
});
|
||||
return data?.services ?? [];
|
||||
|
|
@ -972,7 +977,7 @@ export async function fetchBrowse(
|
|||
path = '/'
|
||||
): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
|
||||
const data = await request({
|
||||
url: `/api/workflows/${instanceId}/connections/${connectionId}/browse`,
|
||||
url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/browse`,
|
||||
method: 'get',
|
||||
params: { service, path },
|
||||
});
|
||||
|
|
@ -986,7 +991,7 @@ export async function fetchClickupTask(
|
|||
taskId: string
|
||||
): Promise<Record<string, unknown>> {
|
||||
const data = await request({
|
||||
url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`,
|
||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/tasks/${encodeURIComponent(taskId)}`,
|
||||
method: 'get',
|
||||
});
|
||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
||||
|
|
@ -999,7 +1004,7 @@ export async function fetchClickupList(
|
|||
listId: string
|
||||
): Promise<Record<string, unknown>> {
|
||||
const data = await request({
|
||||
url: `/api/clickup/${connectionId}/lists/${listId}`,
|
||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}`,
|
||||
method: 'get',
|
||||
});
|
||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
||||
|
|
@ -1012,7 +1017,7 @@ export async function fetchClickupTeam(
|
|||
teamId: string
|
||||
): Promise<Record<string, unknown>> {
|
||||
const data = await request({
|
||||
url: `/api/clickup/${connectionId}/teams/${teamId}`,
|
||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/teams/${encodeURIComponent(teamId)}`,
|
||||
method: 'get',
|
||||
});
|
||||
return data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
||||
|
|
@ -1025,7 +1030,7 @@ export async function fetchClickupListFields(
|
|||
listId: string
|
||||
): Promise<{ fields?: unknown[] } & Record<string, unknown>> {
|
||||
const data = await request({
|
||||
url: `/api/clickup/${connectionId}/lists/${listId}/fields`,
|
||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/fields`,
|
||||
method: 'get',
|
||||
});
|
||||
return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record<string, unknown>;
|
||||
|
|
@ -1046,7 +1051,7 @@ export async function fetchClickupListTasks(
|
|||
{ tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record<string, unknown>
|
||||
> {
|
||||
const data = await request({
|
||||
url: `/api/clickup/${connectionId}/lists/${listId}/tasks`,
|
||||
url: `/api/clickup/${_encodedConnectionId(connectionId)}/lists/${encodeURIComponent(listId)}/tasks`,
|
||||
method: 'get',
|
||||
params: {
|
||||
page: options?.page ?? 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
/**
|
||||
* clickupList — hierarchical ClickUp list picker via connector browse API.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
import {
|
||||
fetchBrowse,
|
||||
fetchClickupList,
|
||||
type BrowseEntry,
|
||||
} from '../../../../api/workflowApi';
|
||||
import type { FieldRendererProps } from './index';
|
||||
import {
|
||||
clickupBrowseParentPath,
|
||||
formatListPickerValue,
|
||||
isClickupContainerEntry,
|
||||
isClickupListEntry,
|
||||
parseClickupListPath,
|
||||
resolveListPathFromValue,
|
||||
} from './clickupPathUtils';
|
||||
|
||||
const CLICKUP_PURPLE = '#7B68EE';
|
||||
|
||||
const glassPanel: React.CSSProperties = {
|
||||
marginTop: 6,
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
||||
background: 'rgba(255, 255, 255, 0.72)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
boxShadow:
|
||||
'0 4px 24px rgba(123, 104, 238, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.5) inset',
|
||||
padding: 8,
|
||||
};
|
||||
|
||||
const glassTrigger: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'stretch',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(123, 104, 238, 0.4)',
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(123,104,238,0.08) 100%)',
|
||||
boxShadow: '0 0 12px rgba(123, 104, 238, 0.15)',
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
export const ClickUpListPicker: React.FC<FieldRendererProps> = ({
|
||||
param,
|
||||
value,
|
||||
onChange,
|
||||
allParams,
|
||||
instanceId,
|
||||
request,
|
||||
onPatchParams,
|
||||
nodeType,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dependsOn = (param.frontendOptions?.dependsOn as string | undefined) || 'connectionReference';
|
||||
const connectionReference = (allParams?.[dependsOn] as string | undefined) || '';
|
||||
const hasConnection = !!connectionReference && typeof connectionReference === 'string';
|
||||
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
const [browsePath, setBrowsePath] = useState('/');
|
||||
const [items, setItems] = useState<BrowseEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pickedLabel, setPickedLabel] = useState<string | null>(null);
|
||||
|
||||
const strVal = typeof value === 'string' ? value : value != null ? String(value) : '';
|
||||
|
||||
const loadBrowse = useCallback(
|
||||
async (path: string) => {
|
||||
if (!request || !instanceId || !connectionReference) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetchBrowse(request, instanceId, connectionReference, 'clickup', path);
|
||||
setItems(res.items);
|
||||
setBrowsePath(res.path || path);
|
||||
} catch (err: unknown) {
|
||||
setItems([]);
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[request, instanceId, connectionReference],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!panelOpen || !hasConnection) return;
|
||||
void loadBrowse(browsePath);
|
||||
}, [panelOpen, hasConnection, browsePath, loadBrowse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!strVal) {
|
||||
setPickedLabel(null);
|
||||
return;
|
||||
}
|
||||
const pathFromVal = resolveListPathFromValue(strVal, param.name);
|
||||
if (pathFromVal) {
|
||||
const parsed = parseClickupListPath(pathFromVal);
|
||||
if (parsed.listId && request && connectionReference) {
|
||||
let cancelled = false;
|
||||
fetchClickupList(request, connectionReference, parsed.listId)
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const name = typeof data.name === 'string' ? data.name : null;
|
||||
setPickedLabel(name || parsed.listId || strVal);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setPickedLabel(parsed.listId || strVal);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
setPickedLabel(parsed.listId || strVal);
|
||||
return;
|
||||
}
|
||||
if (param.name === 'listId' && strVal && request && connectionReference) {
|
||||
let cancelled = false;
|
||||
fetchClickupList(request, connectionReference, strVal)
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
setPickedLabel(typeof data.name === 'string' ? data.name : strVal);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setPickedLabel(strVal);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
setPickedLabel(strVal);
|
||||
}, [strVal, param.name, request, connectionReference]);
|
||||
|
||||
const shouldPatchTeamId =
|
||||
nodeType === 'clickup.searchTasks' || Object.prototype.hasOwnProperty.call(allParams ?? {}, 'teamId');
|
||||
|
||||
const selectList = useCallback(
|
||||
(entry: BrowseEntry) => {
|
||||
const listPath = entry.path;
|
||||
const stored = formatListPickerValue(listPath, param.name);
|
||||
const { teamId, listId } = parseClickupListPath(listPath);
|
||||
|
||||
if (shouldPatchTeamId && onPatchParams && teamId) {
|
||||
const patch: Record<string, unknown> = { [param.name]: stored };
|
||||
patch.teamId = teamId;
|
||||
onPatchParams(patch);
|
||||
} else {
|
||||
onChange(stored);
|
||||
}
|
||||
|
||||
setPickedLabel(entry.name || listId || stored);
|
||||
setPanelOpen(false);
|
||||
},
|
||||
[param.name, shouldPatchTeamId, onPatchParams, onChange],
|
||||
);
|
||||
|
||||
const navigateInto = useCallback((entry: BrowseEntry) => {
|
||||
if (!isClickupContainerEntry(entry.metadata, entry.isFolder)) return;
|
||||
setBrowsePath(entry.path);
|
||||
}, []);
|
||||
|
||||
const goUp = useCallback(() => {
|
||||
setBrowsePath((p) => clickupBrowseParentPath(p));
|
||||
}, []);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
if (shouldPatchTeamId && onPatchParams) {
|
||||
const patch: Record<string, unknown> = { [param.name]: '' };
|
||||
if (nodeType === 'clickup.searchTasks') {
|
||||
patch.teamId = '';
|
||||
}
|
||||
onPatchParams(patch);
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
setPickedLabel(null);
|
||||
}, [shouldPatchTeamId, onPatchParams, onChange, param.name, nodeType]);
|
||||
|
||||
const triggerLabel = strVal
|
||||
? pickedLabel ?? '…'
|
||||
: t('ClickUp-Liste wählen');
|
||||
|
||||
const breadcrumb =
|
||||
browsePath === '/'
|
||||
? t('Workspaces')
|
||||
: browsePath.replace(/^\/team\//, '').replace(/\//g, ' › ');
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>
|
||||
{param.description || param.name}
|
||||
</label>
|
||||
{!request || !instanceId ? (
|
||||
<div style={{ fontSize: 11, color: '#888' }}>
|
||||
{t('Listen-Browser nicht verfügbar (keine API-Anbindung).')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ ...glassTrigger, opacity: hasConnection ? 1 : 0.55 }}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hasConnection}
|
||||
onClick={() => {
|
||||
if (!hasConnection) return;
|
||||
setPanelOpen((o) => {
|
||||
if (!o) setBrowsePath('/');
|
||||
return !o;
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
padding: '8px 10px',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: hasConnection ? 'pointer' : 'not-allowed',
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
color: 'var(--text-primary, #334155)',
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
|
||||
{hasConnection ? triggerLabel : t('Zuerst {field} wählen', { field: dependsOn })}
|
||||
</span>
|
||||
<span aria-hidden style={{ flexShrink: 0, fontSize: 10, opacity: 0.65 }}>
|
||||
{panelOpen ? '▾' : '▸'}
|
||||
</span>
|
||||
</button>
|
||||
{strVal ? (
|
||||
<button
|
||||
type="button"
|
||||
title={t('Auswahl aufheben')}
|
||||
aria-label={t('Auswahl aufheben')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearSelection();
|
||||
}}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 36,
|
||||
border: 'none',
|
||||
borderLeft: '1px solid rgba(123, 104, 238, 0.25)',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: 16,
|
||||
lineHeight: 1,
|
||||
color: 'var(--text-secondary, #64748b)',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{panelOpen && hasConnection && (
|
||||
<div style={glassPanel}>
|
||||
{error && (
|
||||
<div style={{ fontSize: 11, color: '#c00', marginBottom: 6 }}>{error}</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{browsePath !== '/' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={goUp}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
||||
background: 'rgba(255,255,255,0.6)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
↑ {t('Hoch')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadBrowse(browsePath)}
|
||||
title={t('Neu laden')}
|
||||
style={{
|
||||
padding: '3px 8px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(123, 104, 238, 0.35)',
|
||||
background: 'rgba(255,255,255,0.6)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<span style={{ fontSize: 11, color: '#555', flex: 1, minWidth: 0 }}>
|
||||
{breadcrumb}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: 220,
|
||||
overflow: 'auto',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgba(123, 104, 238, 0.2)',
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<div style={{ padding: 8, fontSize: 11, color: '#888' }}>{t('Lade')}</div>
|
||||
)}
|
||||
{!loading && items.length === 0 && (
|
||||
<div style={{ padding: 8, fontSize: 11, color: '#888' }}>
|
||||
{t('Keine Einträge')}
|
||||
</div>
|
||||
)}
|
||||
{!loading &&
|
||||
items.map((item) => {
|
||||
const isList = isClickupListEntry(item.metadata);
|
||||
const canNavigate = isClickupContainerEntry(item.metadata, item.isFolder);
|
||||
return (
|
||||
<div
|
||||
key={item.path}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px 8px',
|
||||
borderBottom: '1px solid rgba(123, 104, 238, 0.08)',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (isList) selectList(item);
|
||||
else if (canNavigate) navigateInto(item);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== 'Enter' && e.key !== ' ') return;
|
||||
e.preventDefault();
|
||||
if (isList) selectList(item);
|
||||
else if (canNavigate) navigateInto(item);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
cursor: isList || canNavigate ? 'pointer' : 'default',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
title={isList ? t('Liste wählen') : canNavigate ? t('Öffnen') : undefined}
|
||||
>
|
||||
{isList ? '📋' : canNavigate ? '📁' : '·'} {item.name}
|
||||
</span>
|
||||
{isList && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectList(item)}
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${CLICKUP_PURPLE}`,
|
||||
background: CLICKUP_PURPLE,
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: 11,
|
||||
boxShadow: '0 0 8px rgba(123, 104, 238, 0.45)',
|
||||
}}
|
||||
>
|
||||
{t('Wählen')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
clickupBrowseParentPath,
|
||||
formatListPickerValue,
|
||||
parseClickupListPath,
|
||||
} from './clickupPathUtils';
|
||||
|
||||
describe('clickupPathUtils', () => {
|
||||
it('parseClickupListPath extracts team and list ids', () => {
|
||||
expect(parseClickupListPath('/team/abc/list/xyz')).toEqual({
|
||||
teamId: 'abc',
|
||||
listId: 'xyz',
|
||||
});
|
||||
expect(parseClickupListPath('')).toEqual({});
|
||||
});
|
||||
|
||||
it('formatListPickerValue stores path or listId by param name', () => {
|
||||
const path = '/team/abc/list/xyz';
|
||||
expect(formatListPickerValue(path, 'pathQuery')).toBe(path);
|
||||
expect(formatListPickerValue(path, 'listId')).toBe('xyz');
|
||||
});
|
||||
|
||||
it('clickupBrowseParentPath walks up hierarchy', () => {
|
||||
expect(clickupBrowseParentPath('/')).toBe('/');
|
||||
expect(clickupBrowseParentPath('/team/t1')).toBe('/');
|
||||
expect(clickupBrowseParentPath('/team/t1/space/s1')).toBe('/team/t1');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/** Parse virtual ClickUp list paths: /team/{teamId}/list/{listId} */
|
||||
|
||||
const LIST_PATH_RE = /^\/team\/([^/]+)\/list\/([^/]+)$/;
|
||||
|
||||
export function parseClickupListPath(path: string): { teamId?: string; listId?: string } {
|
||||
const p = (path || '').trim();
|
||||
const m = p.match(LIST_PATH_RE);
|
||||
if (!m) return {};
|
||||
return { teamId: m[1], listId: m[2] };
|
||||
}
|
||||
|
||||
/** Store path for pathQuery; raw list id for listId param. */
|
||||
export function formatListPickerValue(listPath: string, paramName: string): string {
|
||||
const { listId } = parseClickupListPath(listPath);
|
||||
if (paramName === 'listId' && listId) return listId;
|
||||
return listPath;
|
||||
}
|
||||
|
||||
/** Resolve list path from stored value (path or legacy raw id). */
|
||||
export function resolveListPathFromValue(value: string, paramName: string): string | null {
|
||||
const v = (value || '').trim();
|
||||
if (!v) return null;
|
||||
if (LIST_PATH_RE.test(v)) return v;
|
||||
if (paramName === 'listId' && /^[a-zA-Z0-9_-]+$/.test(v)) {
|
||||
return null;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export function clickupBrowseParentPath(path: string): string {
|
||||
const p = (path || '/').trim() || '/';
|
||||
if (p === '/') return '/';
|
||||
const folder = p.match(/^(\/team\/[^/]+\/space\/[^/]+)\/folder\/[^/]+$/);
|
||||
if (folder) return folder[1];
|
||||
const space = p.match(/^(\/team\/[^/]+)\/space\/[^/]+$/);
|
||||
if (space) return space[1];
|
||||
if (/^\/team\/[^/]+$/.test(p)) return '/';
|
||||
const parts = p.split('/').filter(Boolean);
|
||||
if (parts.length <= 1) return '/';
|
||||
parts.pop();
|
||||
return `/${parts.join('/')}`;
|
||||
}
|
||||
|
||||
export function cuTypeFromEntry(metadata?: Record<string, unknown>): string {
|
||||
const t = metadata?.cuType;
|
||||
return typeof t === 'string' ? t : '';
|
||||
}
|
||||
|
||||
export function isClickupListEntry(metadata?: Record<string, unknown>): boolean {
|
||||
return cuTypeFromEntry(metadata) === 'list';
|
||||
}
|
||||
|
||||
export function isClickupContainerEntry(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
isFolder: boolean,
|
||||
): boolean {
|
||||
const cu = cuTypeFromEntry(metadata);
|
||||
if (cu === 'list') return false;
|
||||
if (cu === 'team' || cu === 'space' || cu === 'folder') return true;
|
||||
return isFolder && cu !== 'task';
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ import { ContextBuilderRenderer } from './ContextBuilderRenderer';
|
|||
import { ContextAssignmentsEditor } from './ContextAssignmentsEditor';
|
||||
import { FeatureInstancePicker } from './FeatureInstancePicker';
|
||||
import { UserFileFolderPicker } from './UserFileFolderPicker';
|
||||
import { ClickUpListPicker } from './ClickUpListPicker';
|
||||
import { ConditionEditor } from './ConditionEditor';
|
||||
import { CaseListEditor } from './CaseListEditor';
|
||||
import { TemplateTextareaRenderer } from './TemplateTextareaRenderer';
|
||||
|
|
@ -1068,7 +1069,7 @@ export const FRONTEND_TYPE_RENDERERS: Record<string, FieldRendererComponent> = {
|
|||
sharepointFolder: SharepointPathPicker,
|
||||
sharepointFile: SharepointPathPicker,
|
||||
userFileFolder: UserFileFolderPicker,
|
||||
clickupList: FolderPicker,
|
||||
clickupList: ClickUpListPicker,
|
||||
clickupTask: FolderPicker,
|
||||
caseList: CaseListEditor,
|
||||
fieldBuilder: FieldBuilderEditor,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export function formatApiError(error: any, defaultMessage: string): string {
|
|||
// Handle other error formats
|
||||
if (typeof data?.detail === 'string') return data.detail;
|
||||
if (typeof data?.message === 'string') return data.message;
|
||||
if (typeof data?.error === 'string') return data.error;
|
||||
if (typeof data === 'string') return data;
|
||||
|
||||
return defaultMessage;
|
||||
|
|
|
|||
Loading…
Reference in a new issue