Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions packages/oc-docs/src/components/Docs/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'prismjs/components/prism-graphql';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-xml-doc';
import 'prismjs/components/prism-python';
import type { HttpRequest } from '@opencollection/types/requests/http';
import type { HttpRequest, HttpRequestParam } from '@opencollection/types/requests/http';
import type { Variable } from '@opencollection/types/common/variables';
import { generateSectionId, getItemId } from '../../../utils/itemUtils';
import {
Expand All @@ -24,7 +24,6 @@ import {
getRequestExamples,
scriptsArrayToObject,
isFolder,
isHttpRequest
} from '../../../utils/schemaHelpers';
import {
MinimalDataTable,
Expand Down Expand Up @@ -218,6 +217,14 @@ const Item = memo(({
examples
};

// Query and path params share one `params` array (distinguished by `type`);
// split in a single pass so each renders in its own labelled table.
const queryParams: HttpRequestParam[] = [];
const pathParams: HttpRequestParam[] = [];
for (const param of endpoint.params || []) {
(param?.type === 'path' ? pathParams : queryParams).push(param);
}

return (
<StyledWrapper
key={itemId}
Expand Down Expand Up @@ -263,9 +270,9 @@ const Item = memo(({

<div className="item-content-main">
<div className="request-details">
{endpoint.params && endpoint.params.length > 0 && (
{queryParams.length > 0 && (
<MinimalDataTable
data={endpoint.params}
data={queryParams}
title="Query Parameters"
columns={[
{ key: 'name', label: 'Name', width: '35%' },
Expand All @@ -275,6 +282,17 @@ const Item = memo(({
/>
)}

{pathParams.length > 0 && (
<MinimalDataTable
data={pathParams}
title="Path Parameters"
columns={[
{ key: 'name', label: 'Name', width: '40%' },
{ key: 'value', label: 'Value', width: '60%' }
]}
/>
)}

{endpoint.headers && endpoint.headers.length > 0 && (
<MinimalDataTable
data={endpoint.headers}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import KeyValueTable, { type KeyValueRow } from '../../../../../ui/KeyValueTable/KeyValueTable';

interface ParamsTabProps {
Expand All @@ -8,39 +8,130 @@ interface ParamsTabProps {
description?: string;
}

interface ParamsSectionProps {
title: string;
description?: string;
data: KeyValueRow[];
onChange: (rows: KeyValueRow[]) => void;
keyLabel?: string;
showEnabled?: boolean;
showActions?: boolean;
disableNewRow?: boolean;
readOnlyKey?: boolean;
}

/**
* A single labelled key/value table. Memoized so that editing one params table
* (e.g. query) doesn't force the sibling table (e.g. path) to re-render while
* its data/handler references are unchanged.
*/
const ParamsSection: React.FC<ParamsSectionProps> = React.memo(({
title,
description,
data,
onChange,
keyLabel = 'Key',
showEnabled = true,
showActions = true,
disableNewRow = false,
readOnlyKey = false
}) => (
<div className="space-y-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
{title}
</span>
{description && (
<span className="text-xs leading-tight" style={{ color: 'var(--text-secondary)' }}>
{description}
</span>
)}
</div>
<KeyValueTable
data={data}
onChange={onChange}
keyPlaceholder={keyLabel}
valuePlaceholder="Value"
showEnabled={showEnabled}
showActions={showActions}
disableNewRow={disableNewRow}
readOnlyKey={readOnlyKey}
/>
</div>
));

ParamsSection.displayName = 'ParamsSection';

export const ParamsTab: React.FC<ParamsTabProps> = ({
params,
onParamsChange,
title = "Query Parameters",
title = 'Query',
description
}) => {
const paramsData: KeyValueRow[] = (params || []).map((param, index) => ({
id: `param-${index}`,
name: param.name || '',
value: param.value || '',
enabled: !param.disabled,
type: param.type || 'query'
}));
// Single pass: split the params into query and path rows. `type` is kept on
// each row so it survives the round-trip back through onParamsChange.
const { queryData, pathData } = useMemo(() => {
const queryData: KeyValueRow[] = [];
const pathData: KeyValueRow[] = [];

(params || [])?.forEach((param, index) => {
const row: KeyValueRow = {
id: `param-${index}`,
name: param.name || '',
value: param.value || '',
enabled: !param.disabled,
type: param.type || 'query'
};
(row.type === 'path' ? pathData : queryData).push(row);
});

return { queryData, pathData };
}, [params]);

// Each table only owns its own subset, so on change we re-tag the edited rows
// and merge them back with the untouched sibling rows before bubbling up.
const handleQueryChange = useCallback(
(rows: KeyValueRow[]) => {
const queryRows = (rows ?? []).map((row) => ({ ...row, type: 'query' }));
onParamsChange([...queryRows, ...pathData]);
},
[onParamsChange, pathData]
);

const handlePathChange = useCallback(
(rows: KeyValueRow[]) => {
const pathRows = (rows ?? []).map((row) => ({ ...row, type: 'path' }));
onParamsChange([...queryData, ...pathRows]);
},
[onParamsChange, queryData]
);

const hasPath = pathData.length > 0;

return (
<div className="space-y-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
{title}
</span>
{description && (
<span className="text-xs leading-tight" style={{ color: 'var(--text-secondary)' }}>
{description}
</span>
)}
</div>
<KeyValueTable
data={paramsData}
onChange={onParamsChange}
keyPlaceholder="Key"
valuePlaceholder="Value"
showEnabled={true}
<div className="space-y-4">
{/* Query table: always shown so query params can be viewed/added
regardless of whether the request currently has any. */}
<ParamsSection
title={title}
description={description}
data={queryData}
onChange={handleQueryChange}
/>

{/* Path table: only shown when the URL actually defines path params. */}
{hasPath && (
<ParamsSection
title="Path"
data={pathData}
onChange={handlePathChange}
keyLabel="Name"
showEnabled={false}
showActions={false}
disableNewRow={true}
readOnlyKey={true}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const Playground: React.FC<PlaygroundProps> = ({ item, collection, selectedEnvir
} finally {
setIsLoading(false);
}
}, [collection, editableItem, runner, selectedEnvironment]);
}, [collection, editableItem, runner, selectedEnvironment, itemUuid]);

const handleMouseDown = useCallback((e: React.MouseEvent) => {
setIsDragging(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import type { HttpRequest } from '@opencollection/types/requests/http';
import { StyledWrapper } from './StyledWrapper';
import { getHttpMethod, getRequestUrl } from '../../../../../../utils/schemaHelpers';
import { getHttpMethod, getRequestUrl, getHttpParams } from '../../../../../../utils/schemaHelpers';
import { syncPathParams } from '../../../../../../utils/pathParams';

interface QueryBarProps {
item: HttpRequest;
Expand All @@ -21,11 +22,18 @@ const QueryBar: React.FC<QueryBarProps> = ({ item, onSendRequest, isLoading, onI

const handleUrlChange = (newUrl: string) => {
setUrl(newUrl);

// Keep the path params in sync with the URL's `:name` segments (add on type,
// remove on delete).
const currentParams = getHttpParams(item);
const syncedParams = syncPathParams(currentParams, newUrl);

const updatedItem = {
...item,
http: {
...item.http,
url: newUrl
url: newUrl,
...(syncedParams !== currentParams ? { params: syncedParams } : {})
}
};
onItemChange(updatedItem);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ const RequestPane: React.FC<RequestPaneProps> = ({ item, onItemChange }) => {

const handleParamsChange = (params: KeyValueRow[]) => {
const updatedParams = params.map(p => ({
name: p.name,
value: p.value,
disabled: !p.enabled,
type: 'query' as const
name: p?.name,
value: p?.value,
disabled: !p?.enabled,
type: p?.type
}));
onItemChange({
...item,
Expand Down
10 changes: 8 additions & 2 deletions packages/oc-docs/src/runner/RequestExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { HttpRequest } from '@opencollection/types/requests/http';
import { RunRequestResponse } from './index';
import { getHttpMethod, getRequestUrl, getHttpHeaders, getHttpBody, getRequestAuth } from '../utils/schemaHelpers';
import { getHttpMethod, getRequestUrl, getHttpHeaders, getHttpBody, getRequestAuth, getHttpParams, getRequestSettings } from '../utils/schemaHelpers';
import { applyPathParams } from '../utils/pathParams';
import stripJsonComments from 'strip-json-comments';

export class RequestExecutor {
Expand All @@ -9,7 +10,12 @@ export class RequestExecutor {

try {
const fetchOptions = await this.buildFetchOptions(request, options.timeout);
const requestUrl = getRequestUrl(request);
// Substitute `:name` path params (e.g. /posts/:postId -> /posts/1) before
// sending. Values are already variable-interpolated by this point. Honour
// the request's encodeUrl setting — only an explicit `false` disables
// encoding (undefined / 'inherit' keep the safe default of encoding).
const encodeUrl = getRequestSettings(request)?.encodeUrl !== false;
const requestUrl = applyPathParams(getRequestUrl(request), getHttpParams(request), { encode: encodeUrl });
const response = await fetch(requestUrl, fetchOptions);
const endTime = Date.now();

Expand Down
2 changes: 1 addition & 1 deletion packages/oc-docs/src/store/slices/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Item as OpenCollectionItem, Folder } from '@opencollection/types/c
import type { HttpRequest } from '@opencollection/types/requests/http';
import type { RootState } from '../store';
import { hydrateWithUUIDs, findAndUpdateItem } from '../../utils/items';
import { isFolder, getItemType } from '../../utils/schemaHelpers';
import { isFolder } from '../../utils/schemaHelpers';

export type ViewMode = 'playground' | 'environments' | 'folder-settings' | 'collection-settings';

Expand Down
15 changes: 15 additions & 0 deletions packages/oc-docs/src/ui/KeyValueTable/KeyValueTable.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@
outline: none;
}

/* Read-only key cell: matches the .text-input box model for column alignment,
but renders as static, non-editable text. */
.key-value-table .text-readonly {
display: block;
width: 100%;
border: 1px solid transparent;
padding: 5px 8px;
font-size: 12px;
color: var(--text-primary);
font-family: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.key-value-table .text-input::placeholder {
color: var(--text-tertiary);
opacity: 0.6;
Expand Down
Loading
Loading