@wordpress/media-fields 0.2.0 → 0.2.1-next.06ee73755.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE.md +1 -1
  2. package/build/alt_text/{index.js → index.cjs} +2 -3
  3. package/build/alt_text/{index.js.map → index.cjs.map} +2 -2
  4. package/build/attached_to/edit.cjs +153 -0
  5. package/build/attached_to/edit.cjs.map +7 -0
  6. package/build/attached_to/index.cjs +49 -0
  7. package/build/attached_to/index.cjs.map +7 -0
  8. package/build/attached_to/view.cjs +49 -0
  9. package/build/attached_to/view.cjs.map +7 -0
  10. package/build/caption/{index.js → index.cjs} +3 -4
  11. package/build/caption/{index.js.map → index.cjs.map} +2 -2
  12. package/build/date_added/index.cjs +41 -0
  13. package/build/date_added/index.cjs.map +7 -0
  14. package/build/date_modified/index.cjs +41 -0
  15. package/build/date_modified/index.cjs.map +7 -0
  16. package/build/description/{index.js → index.cjs} +3 -4
  17. package/build/description/{index.js.map → index.cjs.map} +2 -2
  18. package/build/filename/{index.js → index.cjs} +2 -2
  19. package/build/filename/{view.js → view.cjs} +1 -1
  20. package/build/filesize/{index.js → index.cjs} +1 -1
  21. package/build/{index.js → index.cjs} +18 -9
  22. package/build/index.cjs.map +7 -0
  23. package/build/media_dimensions/{index.js → index.cjs} +1 -1
  24. package/build/media_thumbnail/{index.js → index.cjs} +2 -2
  25. package/build/media_thumbnail/{view.js → view.cjs} +43 -37
  26. package/build/media_thumbnail/view.cjs.map +7 -0
  27. package/build/mime_type/{index.js → index.cjs} +4 -2
  28. package/build/mime_type/{index.js.map → index.cjs.map} +2 -2
  29. package/build/{types.js → types.cjs} +1 -1
  30. package/build/utils/{get-media-type-from-mime-type.js → get-media-type-from-mime-type.cjs} +1 -1
  31. package/build/utils/{get-raw-content.js → get-raw-content.cjs} +1 -1
  32. package/build/utils/{get-rendered-content.js → get-rendered-content.cjs} +1 -1
  33. package/build-module/alt_text/{index.js → index.mjs} +2 -3
  34. package/build-module/alt_text/{index.js.map → index.mjs.map} +2 -2
  35. package/build-module/attached_to/edit.mjs +135 -0
  36. package/build-module/attached_to/edit.mjs.map +7 -0
  37. package/build-module/attached_to/index.mjs +18 -0
  38. package/build-module/attached_to/index.mjs.map +7 -0
  39. package/build-module/attached_to/view.mjs +28 -0
  40. package/build-module/attached_to/view.mjs.map +7 -0
  41. package/build-module/caption/{index.js → index.mjs} +3 -4
  42. package/build-module/caption/{index.js.map → index.mjs.map} +2 -2
  43. package/build-module/date_added/index.mjs +20 -0
  44. package/build-module/date_added/index.mjs.map +7 -0
  45. package/build-module/date_modified/index.mjs +20 -0
  46. package/build-module/date_modified/index.mjs.map +7 -0
  47. package/build-module/description/{index.js → index.mjs} +3 -4
  48. package/build-module/description/{index.js.map → index.mjs.map} +2 -2
  49. package/build-module/filename/{index.js → index.mjs} +2 -2
  50. package/build-module/filename/{view.js → view.mjs} +1 -1
  51. package/build-module/filesize/{index.js → index.mjs} +1 -1
  52. package/build-module/index.mjs +26 -0
  53. package/build-module/index.mjs.map +7 -0
  54. package/build-module/media_dimensions/{index.js → index.mjs} +1 -1
  55. package/build-module/media_thumbnail/{index.js → index.mjs} +2 -2
  56. package/build-module/media_thumbnail/{view.js → view.mjs} +43 -37
  57. package/build-module/media_thumbnail/view.mjs.map +7 -0
  58. package/build-module/mime_type/{index.js → index.mjs} +4 -2
  59. package/build-module/mime_type/{index.js.map → index.mjs.map} +2 -2
  60. package/build-module/types.mjs +1 -0
  61. package/build-module/utils/{get-media-type-from-mime-type.js → get-media-type-from-mime-type.mjs} +1 -1
  62. package/build-module/utils/{get-raw-content.js → get-raw-content.mjs} +1 -1
  63. package/build-module/utils/{get-rendered-content.js → get-rendered-content.mjs} +1 -1
  64. package/build-style/style-rtl.css +4 -0
  65. package/build-style/style.css +4 -0
  66. package/build-types/alt_text/index.d.ts.map +1 -1
  67. package/build-types/attached_to/edit.d.ts +29 -0
  68. package/build-types/attached_to/edit.d.ts.map +1 -0
  69. package/build-types/attached_to/index.d.ts +8 -0
  70. package/build-types/attached_to/index.d.ts.map +1 -0
  71. package/build-types/attached_to/view.d.ts +7 -0
  72. package/build-types/attached_to/view.d.ts.map +1 -0
  73. package/build-types/caption/index.d.ts.map +1 -1
  74. package/build-types/date_added/index.d.ts +8 -0
  75. package/build-types/date_added/index.d.ts.map +1 -0
  76. package/build-types/date_modified/index.d.ts +8 -0
  77. package/build-types/date_modified/index.d.ts.map +1 -0
  78. package/build-types/description/index.d.ts.map +1 -1
  79. package/build-types/filename/test/index.test.d.ts +2 -0
  80. package/build-types/filename/test/index.test.d.ts.map +1 -0
  81. package/build-types/filename/test/view.test.d.ts +2 -0
  82. package/build-types/filename/test/view.test.d.ts.map +1 -0
  83. package/build-types/filesize/test/index.test.d.ts +2 -0
  84. package/build-types/filesize/test/index.test.d.ts.map +1 -0
  85. package/build-types/index.d.ts +3 -0
  86. package/build-types/index.d.ts.map +1 -1
  87. package/build-types/media_dimensions/test/index.test.d.ts +2 -0
  88. package/build-types/media_dimensions/test/index.test.d.ts.map +1 -0
  89. package/build-types/media_thumbnail/view.d.ts.map +1 -1
  90. package/build-types/mime_type/index.d.ts.map +1 -1
  91. package/build-types/stories/index.story.d.ts.map +1 -1
  92. package/package.json +30 -15
  93. package/src/alt_text/index.tsx +0 -1
  94. package/src/attached_to/edit.tsx +185 -0
  95. package/src/attached_to/index.tsx +24 -0
  96. package/src/attached_to/view.tsx +43 -0
  97. package/src/caption/index.tsx +0 -1
  98. package/src/date_added/index.tsx +26 -0
  99. package/src/date_modified/index.tsx +26 -0
  100. package/src/description/index.tsx +0 -1
  101. package/src/filename/test/index.test.ts +59 -0
  102. package/src/filename/test/view.test.tsx +87 -0
  103. package/src/filesize/test/index.test.tsx +107 -0
  104. package/src/index.ts +3 -0
  105. package/src/media_dimensions/test/index.test.ts +132 -0
  106. package/src/media_thumbnail/view.tsx +63 -46
  107. package/src/mime_type/index.ts +3 -1
  108. package/src/stories/index.story.tsx +85 -2
  109. package/build/index.js.map +0 -7
  110. package/build/media_thumbnail/view.js.map +0 -7
  111. package/build-module/index.js +0 -20
  112. package/build-module/index.js.map +0 -7
  113. package/build-module/media_thumbnail/view.js.map +0 -7
  114. package/build-module/types.js +0 -1
  115. package/tsconfig.json +0 -31
  116. package/tsconfig.tsbuildinfo +0 -1
  117. /package/build/filename/{index.js.map → index.cjs.map} +0 -0
  118. /package/build/filename/{view.js.map → view.cjs.map} +0 -0
  119. /package/build/filesize/{index.js.map → index.cjs.map} +0 -0
  120. /package/build/media_dimensions/{index.js.map → index.cjs.map} +0 -0
  121. /package/build/media_thumbnail/{index.js.map → index.cjs.map} +0 -0
  122. /package/build/{types.js.map → types.cjs.map} +0 -0
  123. /package/build/utils/{get-media-type-from-mime-type.js.map → get-media-type-from-mime-type.cjs.map} +0 -0
  124. /package/build/utils/{get-raw-content.js.map → get-raw-content.cjs.map} +0 -0
  125. /package/build/utils/{get-rendered-content.js.map → get-rendered-content.cjs.map} +0 -0
  126. /package/build-module/filename/{index.js.map → index.mjs.map} +0 -0
  127. /package/build-module/filename/{view.js.map → view.mjs.map} +0 -0
  128. /package/build-module/filesize/{index.js.map → index.mjs.map} +0 -0
  129. /package/build-module/media_dimensions/{index.js.map → index.mjs.map} +0 -0
  130. /package/build-module/media_thumbnail/{index.js.map → index.mjs.map} +0 -0
  131. /package/build-module/{types.js.map → types.mjs.map} +0 -0
  132. /package/build-module/utils/{get-media-type-from-mime-type.js.map → get-media-type-from-mime-type.mjs.map} +0 -0
  133. /package/build-module/utils/{get-raw-content.js.map → get-raw-content.mjs.map} +0 -0
  134. /package/build-module/utils/{get-rendered-content.js.map → get-rendered-content.mjs.map} +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __experimentalFetchLinkSuggestions as fetchLinkSuggestions,
6
+ store as coreStore,
7
+ } from '@wordpress/core-data';
8
+ import { Button, ComboboxControl } from '@wordpress/components';
9
+ import { __ } from '@wordpress/i18n';
10
+ import { useState, createInterpolateElement } from '@wordpress/element';
11
+ import { debounce } from '@wordpress/compose';
12
+ import { useSelect } from '@wordpress/data';
13
+ import type { DataFormControlProps } from '@wordpress/dataviews';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import type { MediaItem } from '../types';
19
+ import { getRenderedContent } from '../utils/get-rendered-content';
20
+
21
+ export type SearchResult = {
22
+ /**
23
+ * Post or term id.
24
+ */
25
+ id: number;
26
+ /**
27
+ * Link url.
28
+ */
29
+ url: string;
30
+ /**
31
+ * Title of the link.
32
+ */
33
+ title: string;
34
+ /**
35
+ * The taxonomy or post type slug or type URL.
36
+ */
37
+ type: string;
38
+ /**
39
+ * Link kind of post-type or taxonomy
40
+ */
41
+ kind?: string;
42
+ };
43
+
44
+ export default function MediaAttachedToEdit( {
45
+ data,
46
+ onChange,
47
+ }: DataFormControlProps< MediaItem > ) {
48
+ const defaultPost =
49
+ !! data.post && !! data?._embedded?.[ 'wp:attached-to' ]?.[ 0 ]
50
+ ? [
51
+ {
52
+ label: getRenderedContent(
53
+ data._embedded?.[ 'wp:attached-to' ]?.[ 0 ]?.title
54
+ ),
55
+ value: data.post.toString(),
56
+ },
57
+ ]
58
+ : [];
59
+ const [ options, setOptions ] =
60
+ useState< { label: string; value: string }[] >( defaultPost );
61
+ const [ searchResults, setSearchResults ] = useState< SearchResult[] >(
62
+ []
63
+ );
64
+ const [ isLoading, setIsLoading ] = useState( false );
65
+ const [ value, setValue ] = useState< string | null >(
66
+ data?.post?.toString() ?? null
67
+ );
68
+
69
+ const postTypes = useSelect(
70
+ ( select ) => select( coreStore ).getPostTypes(),
71
+ []
72
+ );
73
+ const handleDetach = () => {
74
+ onChange( {
75
+ post: 0,
76
+ _embedded: { ...data?._embedded, 'wp:attached-to': undefined },
77
+ } );
78
+ setOptions( [] );
79
+ };
80
+
81
+ const onValueChange = async ( filterValue: string ) => {
82
+ setIsLoading( true );
83
+ const results = await fetchLinkSuggestions(
84
+ filterValue,
85
+ /*
86
+ * @TODO `fetchLinkSuggestions()` should accept `perPage` as an option argument.
87
+ * `isInitialSuggestions` limits the result to 3, otherwise it's hardcoded to 20.
88
+ */
89
+ { type: 'post', isInitialSuggestions: true },
90
+ {}
91
+ );
92
+ setSearchResults( results );
93
+ const mappedSuggestions = results.map( ( result ) => {
94
+ return {
95
+ label: result.title,
96
+ value: result.id.toString(),
97
+ };
98
+ } );
99
+ setOptions( mappedSuggestions );
100
+ setIsLoading( false );
101
+ };
102
+
103
+ /**
104
+ * Handle selection.
105
+ *
106
+ * @param {Object} selectedPostId The selected post id.
107
+ */
108
+ const handleSelectOption = (
109
+ selectedPostId: string | null | undefined
110
+ ) => {
111
+ if ( ! selectedPostId ) {
112
+ handleDetach();
113
+ return;
114
+ }
115
+ setValue( selectedPostId );
116
+ if ( selectedPostId ) {
117
+ const selectedPost = searchResults.find(
118
+ ( result ) => result.id === Number( selectedPostId )
119
+ );
120
+ // Although unlikely, it's technically possible for selectedPost to not be found.
121
+ // E.g. if the user selects an option just as new search results are loaded.
122
+ // TODO: Add error handling for when selectedPost is not found.
123
+ if ( selectedPost && postTypes ) {
124
+ const postType = postTypes.find(
125
+ ( _postType: { slug: string } ) =>
126
+ _postType.slug === selectedPost?.type
127
+ );
128
+
129
+ const attachedTo = {
130
+ ...( postType && { type: postType.slug } ),
131
+ id: Number( selectedPostId ),
132
+ title: {
133
+ raw: selectedPost.title,
134
+ rendered: selectedPost.title,
135
+ },
136
+ };
137
+
138
+ onChange( {
139
+ post: Number( selectedPostId ),
140
+ _embedded: {
141
+ ...data?._embedded,
142
+ 'wp:attached-to': [ attachedTo ],
143
+ },
144
+ } );
145
+ }
146
+ }
147
+ };
148
+
149
+ const help = !! data.post
150
+ ? createInterpolateElement(
151
+ __(
152
+ 'Search for a post or page to attach this media to or <button>detach current</button>.'
153
+ ),
154
+ {
155
+ button: (
156
+ <Button
157
+ __next40pxDefaultSize
158
+ onClick={ handleDetach }
159
+ variant="link"
160
+ accessibleWhenDisabled
161
+ />
162
+ ),
163
+ }
164
+ )
165
+ : __( 'Search for a post or page to attach this media to.' );
166
+
167
+ return (
168
+ <ComboboxControl
169
+ className="dataviews-media-field__attached-to"
170
+ __next40pxDefaultSize
171
+ isLoading={ isLoading }
172
+ label={ __( 'Attached to' ) }
173
+ help={ help }
174
+ value={ value }
175
+ options={ options }
176
+ onFilterValueChange={ debounce(
177
+ ( filterValue: unknown ) =>
178
+ onValueChange( filterValue as string ),
179
+ 300
180
+ ) }
181
+ onChange={ handleSelectOption }
182
+ hideLabelFromVision
183
+ />
184
+ );
185
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import type { Field } from '@wordpress/dataviews';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import type { MediaItem } from '../types';
11
+ import MediaAttachedToView from './view';
12
+ import MediaAttachedToEdit from './edit';
13
+
14
+ const attachedToField: Partial< Field< MediaItem > > = {
15
+ id: 'attached_to',
16
+ type: 'text',
17
+ label: __( 'Attached to' ),
18
+ Edit: MediaAttachedToEdit,
19
+ render: MediaAttachedToView,
20
+ enableSorting: false,
21
+ filterBy: false,
22
+ };
23
+
24
+ export default attachedToField;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useEffect } from '@wordpress/element';
5
+ import { __ } from '@wordpress/i18n';
6
+ import type { DataViewRenderFieldProps } from '@wordpress/dataviews';
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import type { MediaItem } from '../types';
11
+ import { getRenderedContent } from '../utils/get-rendered-content';
12
+
13
+ export default function MediaAttachedToView( {
14
+ item,
15
+ }: DataViewRenderFieldProps< MediaItem > ) {
16
+ // Store the displayed title in state, as the embedded post may be loaded
17
+ // asynchronously. This ensures that the title remains stable after it
18
+ // is updated by the user, and while it is re-fetched from the server.
19
+ const [ attachedPostTitle, setAttachedPostTitle ] = useState<
20
+ string | null
21
+ >( null );
22
+
23
+ const parentId = item.post;
24
+ const embeddedPostId = item._embedded?.[ 'wp:attached-to' ]?.[ 0 ]?.id;
25
+ const embeddedPostTitle =
26
+ item._embedded?.[ 'wp:attached-to' ]?.[ 0 ]?.title;
27
+
28
+ useEffect( () => {
29
+ if ( !! parentId && parentId === embeddedPostId ) {
30
+ setAttachedPostTitle(
31
+ getRenderedContent( embeddedPostTitle ) ||
32
+ embeddedPostId?.toString() ||
33
+ ''
34
+ );
35
+ }
36
+
37
+ if ( ! parentId ) {
38
+ setAttachedPostTitle( __( '(Unattached)' ) );
39
+ }
40
+ }, [ parentId, embeddedPostId, embeddedPostTitle ] );
41
+
42
+ return <>{ attachedPostTitle }</>;
43
+ }
@@ -24,7 +24,6 @@ const captionField: Partial< Field< Updatable< Attachment > > > = {
24
24
  value={ getRawContent( data.caption ) || '' }
25
25
  onChange={ ( value ) => onChange( { caption: value } ) }
26
26
  rows={ 2 }
27
- __nextHasNoMarginBottom
28
27
  />
29
28
  );
30
29
  },
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { getSettings } from '@wordpress/date';
6
+ import type { Field } from '@wordpress/dataviews';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import type { MediaItem } from '../types';
12
+
13
+ const dateAddedField: Partial< Field< MediaItem > > = {
14
+ id: 'date',
15
+ type: 'datetime',
16
+ label: __( 'Date added' ),
17
+ filterBy: {
18
+ operators: [ 'before', 'after' ],
19
+ },
20
+ format: {
21
+ datetime: getSettings().formats.datetimeAbbreviated,
22
+ },
23
+ readOnly: true,
24
+ };
25
+
26
+ export default dateAddedField;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+ import { getSettings } from '@wordpress/date';
6
+ import type { Field } from '@wordpress/dataviews';
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import type { MediaItem } from '../types';
12
+
13
+ const dateModifiedField: Partial< Field< MediaItem > > = {
14
+ id: 'modified',
15
+ type: 'datetime',
16
+ label: __( 'Date modified' ),
17
+ filterBy: {
18
+ operators: [ 'before', 'after' ],
19
+ },
20
+ format: {
21
+ datetime: getSettings().formats.datetimeAbbreviated,
22
+ },
23
+ readOnly: true,
24
+ };
25
+
26
+ export default dateModifiedField;
@@ -26,7 +26,6 @@ const descriptionField: Partial< Field< Updatable< Attachment > > > = {
26
26
  value={ getRawContent( data.description ) || '' }
27
27
  onChange={ ( value ) => onChange( { description: value } ) }
28
28
  rows={ 5 }
29
- __nextHasNoMarginBottom
30
29
  />
31
30
  );
32
31
  },
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import filenameField from '../index';
5
+ import type { MediaItem } from '../../types';
6
+
7
+ describe( 'filenameField', () => {
8
+ it( 'has correct field configuration', () => {
9
+ expect( filenameField ).toMatchObject( {
10
+ id: 'filename',
11
+ type: 'text',
12
+ label: 'File name',
13
+ enableSorting: false,
14
+ filterBy: false,
15
+ readOnly: true,
16
+ } );
17
+ } );
18
+
19
+ describe( 'getValue', () => {
20
+ it( 'extracts filename from source_url', () => {
21
+ const item: Partial< MediaItem > = {
22
+ source_url:
23
+ 'https://example.com/wp-content/uploads/2024/image.jpg',
24
+ };
25
+
26
+ const result = filenameField.getValue?.( {
27
+ item: item as MediaItem,
28
+ } );
29
+
30
+ expect( result ).toBe( 'image.jpg' );
31
+ } );
32
+
33
+ it( 'returns undefined when source_url is undefined', () => {
34
+ const item: Partial< MediaItem > = {};
35
+
36
+ const result = filenameField.getValue?.( {
37
+ item: item as MediaItem,
38
+ } );
39
+
40
+ expect( result ).toBeUndefined();
41
+ } );
42
+
43
+ it( 'returns undefined when source_url is empty string', () => {
44
+ const item: Partial< MediaItem > = {
45
+ source_url: '',
46
+ };
47
+
48
+ const result = filenameField.getValue?.( {
49
+ item: item as MediaItem,
50
+ } );
51
+
52
+ expect( result ).toBeUndefined();
53
+ } );
54
+ } );
55
+
56
+ it( 'has a render function', () => {
57
+ expect( filenameField.render ).toBeDefined();
58
+ } );
59
+ } );
@@ -0,0 +1,87 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import type { NormalizedField } from '@wordpress/dataviews';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import FileNameView from '../view';
15
+ import filenameField from '../index';
16
+ import type { MediaItem } from '../../types';
17
+
18
+ describe( 'FileNameView', () => {
19
+ describe( 'filename rendering', () => {
20
+ it( 'renders short filename (15 characters or less)', () => {
21
+ const item: Partial< MediaItem > = {
22
+ source_url: 'https://example.com/uploads/12345678901.jpg', // exactly 15 chars
23
+ };
24
+
25
+ render(
26
+ <FileNameView
27
+ item={ item as MediaItem }
28
+ field={ filenameField as NormalizedField< MediaItem > }
29
+ />
30
+ );
31
+
32
+ // Verify the filename is visible to users
33
+ expect( screen.getByText( '12345678901.jpg' ) ).toBeInTheDocument();
34
+ } );
35
+
36
+ it( 'renders long filename (more than 15 characters)', () => {
37
+ const longFilename =
38
+ 'very-long-filename-that-exceeds-fifteen-characters.jpg';
39
+ const item: Partial< MediaItem > = {
40
+ source_url: `https://example.com/uploads/${ longFilename }`,
41
+ };
42
+
43
+ render(
44
+ <FileNameView
45
+ item={ item as MediaItem }
46
+ field={ filenameField as NormalizedField< MediaItem > }
47
+ />
48
+ );
49
+
50
+ // Verify the full filename text is accessible to users
51
+ // (the component handles truncation via Truncate/Tooltip, but the text is still present)
52
+ expect( screen.getByText( longFilename ) ).toBeInTheDocument();
53
+ } );
54
+ } );
55
+
56
+ describe( 'edge cases', () => {
57
+ it( 'renders nothing when source_url is missing', () => {
58
+ const item: Partial< MediaItem > = {};
59
+
60
+ const { container } = render(
61
+ <FileNameView
62
+ item={ item as MediaItem }
63
+ field={ filenameField as NormalizedField< MediaItem > }
64
+ />
65
+ );
66
+
67
+ // When there's no source_url, component should render nothing
68
+ expect( container ).toBeEmptyDOMElement();
69
+ } );
70
+
71
+ it( 'renders nothing when source_url is empty', () => {
72
+ const item: Partial< MediaItem > = {
73
+ source_url: '',
74
+ };
75
+
76
+ const { container } = render(
77
+ <FileNameView
78
+ item={ item as MediaItem }
79
+ field={ filenameField as NormalizedField< MediaItem > }
80
+ />
81
+ );
82
+
83
+ // When source_url is empty, component should render nothing
84
+ expect( container ).toBeEmptyDOMElement();
85
+ } );
86
+ } );
87
+ } );
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import filesizeField from '../index';
5
+ import type { MediaItem } from '../../types';
6
+
7
+ describe( 'filesizeField', () => {
8
+ it( 'has correct field configuration', () => {
9
+ expect( filesizeField ).toMatchObject( {
10
+ id: 'filesize',
11
+ type: 'text',
12
+ label: 'File size',
13
+ enableSorting: false,
14
+ filterBy: false,
15
+ readOnly: true,
16
+ } );
17
+ } );
18
+
19
+ describe( 'getValue - byte formatting logic', () => {
20
+ it( 'returns empty string for 0 bytes due to truthy check', () => {
21
+ const item = {
22
+ media_details: {
23
+ filesize: 0,
24
+ sizes: {},
25
+ },
26
+ } as MediaItem;
27
+
28
+ const result = filesizeField.getValue?.( {
29
+ item,
30
+ } );
31
+
32
+ expect( result ).toBe( '' );
33
+ } );
34
+
35
+ it.each( [
36
+ [ 512, /^512\s+B$/, 'bytes (less than 1 KB)' ],
37
+ [ 1024 * 50, /^50\s+KB$/, '50 kilobytes' ],
38
+ [ 1024 * 1024 * 5, /^5\s+MB$/, '5 megabytes' ],
39
+ [ 1024 * 1024 * 1024 * 2.5, /^2\.5\s+GB$/, '2.5 gigabytes' ],
40
+ [ 1024 * 1024 * 1024 * 1024 * 1.5, /^1\.5\s+TB$/, '1.5 terabytes' ],
41
+ [
42
+ 1024 * 1024 * 3.14159,
43
+ /^3\.14\s+MB$/,
44
+ 'fractional sizes with proper decimals',
45
+ ],
46
+ [ 1024, /^1\s+KB$/, 'boundary value (exactly 1 KB)' ],
47
+ ] )(
48
+ 'formats %s bytes correctly: %s',
49
+ ( filesize, expected, description ) => {
50
+ const item = {
51
+ media_details: {
52
+ filesize,
53
+ sizes: {},
54
+ },
55
+ } as MediaItem;
56
+
57
+ const result = filesizeField.getValue?.( {
58
+ item,
59
+ } );
60
+
61
+ try {
62
+ expect( result ).toMatch( expected );
63
+ } catch ( error ) {
64
+ const message =
65
+ error instanceof Error
66
+ ? error.message
67
+ : String( error );
68
+ throw new Error(
69
+ `Failed to format filesize (${ description }): ${ message }`
70
+ );
71
+ }
72
+ }
73
+ );
74
+
75
+ it.each( [
76
+ [ { media_details: { sizes: {} } }, 'when filesize is missing' ],
77
+ [ {}, 'when media_details is missing' ],
78
+ ] )( 'returns empty string %s', ( item, description ) => {
79
+ const result = filesizeField.getValue?.( {
80
+ item: item as MediaItem,
81
+ } );
82
+
83
+ try {
84
+ expect( result ).toBe( '' );
85
+ } catch ( error ) {
86
+ const message =
87
+ error instanceof Error ? error.message : String( error );
88
+ throw new Error(
89
+ `Failed getValue test (${ description }): ${ message }`
90
+ );
91
+ }
92
+ } );
93
+ } );
94
+
95
+ describe( 'isVisible', () => {
96
+ it.each( [
97
+ [ { media_details: { filesize: 1024, sizes: {} } }, true ],
98
+ [ { media_details: { filesize: 0, sizes: {} } }, false ],
99
+ [ { media_details: { sizes: {} } }, false ],
100
+ [ {}, false ],
101
+ ] )( 'returns %s for item %#', ( item, expected ) => {
102
+ const result = filesizeField.isVisible?.( item as MediaItem );
103
+
104
+ expect( result ).toBe( expected );
105
+ } );
106
+ } );
107
+ } );
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { default as altTextField } from './alt_text';
2
+ export { default as attachedToField } from './attached_to';
2
3
  export { default as captionField } from './caption';
4
+ export { default as dateAddedField } from './date_added';
5
+ export { default as dateModifiedField } from './date_modified';
3
6
  export { default as descriptionField } from './description';
4
7
  export { default as filenameField } from './filename';
5
8
  export { default as filesizeField } from './filesize';