Skip to content
Open
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
"@box/metadata-editor": "^2.7.0",
"@box/metadata-filter": "^1.80.23",
"@box/metadata-taxonomy-picker": "^3.3.31",
"@box/metadata-template-browser": "^2.3.31",
"@box/metadata-template-browser": "^3.0.0",
"@box/metadata-template-editor": "^2.5.0",
"@box/metadata-view": "^1.53.26",
"@box/react-virtualized": "^9.22.3-rc-box.10",
Expand Down Expand Up @@ -318,7 +318,7 @@
"@box/metadata-editor": "^2.7.0",
"@box/metadata-filter": "^1.80.23",
"@box/metadata-taxonomy-picker": "^3.3.31",
"@box/metadata-template-browser": "^2.3.31",
"@box/metadata-template-browser": "^3.0.0",
"@box/metadata-template-editor": "^2.5.0",
"@box/metadata-view": "^1.53.26",
"@box/react-virtualized": "^9.22.3-rc-box.10",
Expand Down Expand Up @@ -388,7 +388,7 @@
"@box/metadata-editor": "^2.7.0",
"@box/user-selector": "3.0.1",
"@box/metadata-taxonomy-picker": "^3.3.31",
"@box/metadata-template-browser": "^2.3.31",
"@box/metadata-template-browser": "^3.0.0",
"@box/metadata-template-editor": "^2.5.0",
"@box/combobox-with-api": "2.2.25",
"@box/box-ai-agent-selector": "2.2.23",
Expand Down
13 changes: 13 additions & 0 deletions src/api/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ class Metadata extends File {
return this.getNamespacesAPI().getMetadataNamespacesUrl(namespaceFqn);
}

/** @see MetadataNamespaces.getNamespace */
getNamespace(file: BoxItem, namespaceFqn: string): Promise<?Object> {
return this.getNamespacesAPI().getNamespace(file, namespaceFqn);
}

/** @see MetadataNamespaces.listNamespaces */
listNamespaces(
file: BoxItem,
Expand All @@ -328,6 +333,14 @@ class Metadata extends File {
return this.getNamespacesAPI().listTemplatesForNamespace(file, namespaceFqn, params);
}

/** @see MetadataNamespaces.searchTemplates */
searchTemplates(
file: BoxItem,
params: { query: string, limit: number, marker?: string },
): Promise<{ entries: Array<Object>, next_marker?: string }> {
return this.getNamespacesAPI().searchTemplates(file, params);
}

/** @see MetadataNamespaces.createMetadataTemplate */
createMetadataTemplate(
file: BoxItem,
Expand Down
56 changes: 54 additions & 2 deletions src/api/MetadataNamespaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,66 @@ export default class MetadataNamespaces {
const response = await this.host.xhr.get({
url,
id: this.getRequestFileId(file),
params: { limit: params.limit, marker: params.marker },
params: { limit: params.limit, marker: params.marker, fields: 'permissions' },
});
return getProp(response, 'data', { entries: [] });
} catch (e) {
return { entries: [] };
}
}

/**
* Fetches one namespace, including `permissions` when the caller may create
* templates under it.
*
* The enterprise root is never a row of `.../children`, so template
* creation at the root is decided from this response. `permissions` is
* omitted unless `fields=permissions` is requested.
*/
async getNamespace(file: BoxItem, namespaceFqn: string): Promise<?Object> {
const url = this.getMetadataNamespacesUrl(namespaceFqn);
try {
const response = await this.host.xhr.get({
url,
id: this.getRequestFileId(file),
params: { fields: 'permissions' },
});
return getProp(response, 'data', null);
} catch (e) {
return null;
}
}

/**
* Typeahead over templates the caller can see.
*
* `GET /metadata_templates/search`. `query` is a required prefix
* matched against `displayName` and `templateKey`. Hits omit template fields
* and hidden templates. A sub-namespace hit carries `containingNamespace`
* and `ancestors` (`fqn` + `displayName` only); enterprise-root, legacy, and
* global hits carry neither. The path is `ancestors` and `containingNamespace`.
* `fields=permissions` asks for the same template permission object as
* template list. The field is absent until that search response includes it.
* Errors propagate so the picker can show its search error state.
*/
async searchTemplates(
file: BoxItem,
params: { query: string, limit: number, marker?: string },
): Promise<{ entries: Array<Object>, next_marker?: string }> {
const url = `${this.host.getMetadataTemplateUrl()}/search`;
const response = await this.host.xhr.get({
url,
id: this.getRequestFileId(file),
params: {
query: params.query,
limit: params.limit,
fields: 'permissions',
...(params.marker ? { marker: params.marker } : {}),
},
});
return getProp(response, 'data', { entries: [] });
}

/**
* Lists templates under a namespace FQN with cursor pagination.
*/
Expand All @@ -90,7 +142,7 @@ export default class MetadataNamespaces {
const response = await this.host.xhr.get({
url,
id: this.getRequestFileId(file),
params: { limit: params.limit, marker: params.marker },
params: { limit: params.limit, marker: params.marker, fields: 'permissions' },
});
return getProp(response, 'data', { entries: [] });
} catch (e) {
Expand Down
75 changes: 74 additions & 1 deletion src/api/__tests__/MetadataNamespaces.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,84 @@ describe('api/MetadataNamespaces', () => {
expect(host.xhr.get).toHaveBeenCalledWith({
url: 'https://api.box.com/2.0/metadata_namespaces/enterprise_1/children',
id: 'file_123',
params: { limit: 20, marker: undefined },
params: { limit: 20, marker: undefined, fields: 'permissions' },
});
});
});

describe('getNamespace()', () => {
test('should GET the namespace and return its grants', async () => {
host.xhr.get.mockResolvedValue({
data: {
fqn: 'enterprise_1',
displayName: 'Enterprise',
permissions: { can_create_templates: true },
},
});

await expect(api.getNamespace(file, 'enterprise_1')).resolves.toEqual({
fqn: 'enterprise_1',
displayName: 'Enterprise',
permissions: { can_create_templates: true },
});
expect(host.xhr.get).toHaveBeenCalledWith({
url: 'https://api.box.com/2.0/metadata_namespaces/enterprise_1',
id: 'file_123',
params: { fields: 'permissions' },
});
});

test('should return null when the namespace request fails', async () => {
host.xhr.get.mockRejectedValue(new Error('not found'));

await expect(api.getNamespace(file, 'enterprise_1')).resolves.toBeNull();
});
});

describe('searchTemplates()', () => {
test('should GET /metadata_templates/search with query, limit, and marker', async () => {
host.xhr.get.mockResolvedValue({
data: {
limit: 20,
next_marker: 'cursor-1',
prev_marker: null,
entries: [],
},
});

await expect(api.searchTemplates(file, { query: 'Le', limit: 20, marker: 'cursor-0' })).resolves.toEqual({
limit: 20,
next_marker: 'cursor-1',
prev_marker: null,
entries: [],
});
expect(host.xhr.get).toHaveBeenCalledWith({
url: 'https://api.box.com/2.0/metadata_templates/search',
id: 'file_123',
params: { query: 'Le', limit: 20, fields: 'permissions', marker: 'cursor-0' },
});
});

test('should omit marker when the caller has no cursor', async () => {
host.xhr.get.mockResolvedValue({ data: { entries: [] } });

await api.searchTemplates(file, { query: 'Le', limit: 20 });

expect(host.xhr.get).toHaveBeenCalledWith({
url: 'https://api.box.com/2.0/metadata_templates/search',
id: 'file_123',
params: { query: 'Le', limit: 20, fields: 'permissions' },
});
});

test('should propagate a failed search', async () => {
const error = new Error('not found');
host.xhr.get.mockRejectedValue(error);

await expect(api.searchTemplates(file, { query: 'Le', limit: 20 })).rejects.toBe(error);
});
});

describe('getTemplateSchemaForEditor()', () => {
test('should map hidden from the live schema response', async () => {
host.metadataNamespaceMode = METADATA_SCOPE_MODE_MIGRATION;
Expand Down
11 changes: 10 additions & 1 deletion src/elements/content-sidebar/MetadataSidebarRedesign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { useMetadataSidebarFilteredTemplates } from './hooks/useMetadataSidebarF
import useMetadataFieldSelection from './hooks/useMetadataFieldSelection';
import useMetadataSidebarUnsavedChangesGuard from './hooks/useMetadataSidebarUnsavedChangesGuard';
import useMetadataTemplateEditor from './hooks/useMetadataTemplateEditor';
import useCanCreateTemplateAtRoot from './hooks/useCanCreateTemplateAtRoot';
import useMetadataTemplateItemsService from './hooks/useMetadataTemplateItemsService';
import { type MetadataTemplateLocator } from './hooks/useMetadataTemplateEventService';
import useMetadataNamespaceContext, { type MetadataScopeMode } from './hooks/useMetadataNamespaceContext';
Expand Down Expand Up @@ -235,6 +236,13 @@ function MetadataSidebarRedesign({
isTemplateManagementEnabled ? enterpriseId : undefined,
templates ?? [],
);
// The browser reads this only when it mounts, so the dropdown is keyed on
// the resolved value and remounts once the root namespace grant arrives.
const canCreateAtRoot = useCanCreateTemplateAtRoot(
api,
file,
isTemplateManagementEnabled ? enterpriseId : undefined,
);

const { handleUnsavedChangesModalOpen, pendingNavLocation, setPendingNavLocation, unblockRouterHistory } =
useMetadataSidebarUnsavedChangesGuard({
Expand Down Expand Up @@ -510,6 +518,7 @@ function MetadataSidebarRedesign({

const metadataDropdown = canEdit && isSuccess && templates && (
<MetadataTemplateDropdown
key={canCreateAtRoot ? 'root-create' : 'root-view'}
templates={templates}
selectedTemplates={appliedTemplateInstances as MetadataTemplate[]}
onSelect={handleTemplateSelect}
Expand All @@ -520,7 +529,7 @@ function MetadataSidebarRedesign({
itemsService={isTemplateManagementEnabled ? itemsService : undefined}
onCreateTemplate={isTemplateManagementEnabled ? handleOpenCreateEditor : undefined}
onEditTemplate={isTemplateManagementEnabled ? handleOpenEditEditor : undefined}
canCreateAtRoot
canCreateAtRoot={canCreateAtRoot}
open={isDropdownOpen}
onOpenChange={setIsDropdownOpen}
/>
Expand Down
Loading
Loading