Skip to content

Commit

Permalink
Improve entry thumbnail handling
Browse files Browse the repository at this point in the history
Fix #307
  • Loading branch information
kyoshino committed Jan 18, 2025
1 parent d9a54b3 commit 23648f6
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/lib/components/contents/list/entry-list-item.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
}}
/>
</GridCell>
{#if collection._thumbnailFieldName}
{#if collection._thumbnailFieldNames.length}
<GridCell class="image">
{#await getEntryThumbnail(collection, entry) then src}
{#if src}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/contents/list/secondary-toolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
: undefined,
);
const collectionName = $derived(entryCollection?.name);
const _thumbnailFieldName = $derived(entryCollection?._thumbnailFieldName);
const thumbnailFieldNames = $derived(entryCollection?._thumbnailFieldNames ?? []);
const hasListedEntries = $derived(!!$listedEntries.length);
const hasMultipleEntries = $derived($listedEntries.length > 1);
</script>
Expand Down Expand Up @@ -58,7 +58,7 @@
/>
{/if}
<ViewSwitcher
disabled={!hasListedEntries || !_thumbnailFieldName}
disabled={!hasListedEntries || !thumbnailFieldNames.length}
{currentView}
aria-controls="entry-list"
/>
Expand Down
45 changes: 37 additions & 8 deletions src/lib/services/contents/collection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@ export const selectedCollection = writable();
*/
const collectionCacheMap = new Map();

/**
* Get a list of field key paths to be used to find an entry thumbnail.
* @param {RawCollection} rawCollection - Raw collection definition.
* @returns {FieldKeyPath[]} Key path list.
*/
const getThumbnailFieldNames = (rawCollection) => {
const { folder, fields, thumbnail } = rawCollection;

if (!folder) {
return [];
}

if (typeof thumbnail === 'string') {
return [thumbnail];
}

// Support multiple field names
if (Array.isArray(thumbnail)) {
return thumbnail;
}

// Collect the names of all non-nested Image/File fields for inference
if (fields?.length) {
return fields
.filter(({ widget = 'string' }) => ['image', 'file'].includes(widget))
.map(({ name }) => name);
}

return [];
};

/**
* Get a collection by name.
* @param {string} name - Collection name.
Expand All @@ -38,7 +69,7 @@ export const getCollection = (name) => {
return undefined;
}

const { fields, thumbnail, folder, files } = rawCollection;
const { folder, files } = rawCollection;

// Normalize folder/file paths by removing leading/trailing slashes
if (isEntryCollection) {
Expand All @@ -59,15 +90,13 @@ export const getCollection = (name) => {

/** @type {Collection} */
const collection = isEntryCollection
? {
? /** @type {EntryCollection} */ ({
...collectionBase,
_type: /** @type {CollectionType} */ ('entry'),
_file: getFileConfig({ rawCollection, _i18n }),
_thumbnailFieldName: rawCollection.folder
? (thumbnail ?? fields?.find(({ widget }) => widget === 'image')?.name)
: undefined,
}
: {
_thumbnailFieldNames: getThumbnailFieldNames(rawCollection),
})
: /** @type {FileCollection} */ ({
...collectionBase,
_type: /** @type {CollectionType} */ ('file'),
_fileMap: /** @type {RawCollectionFile[]} */ (files)?.length
Expand All @@ -80,7 +109,7 @@ export const getCollection = (name) => {
}),
)
: {},
};
});

collectionCacheMap.set(name, collection);

Expand Down
1 change: 1 addition & 0 deletions src/lib/services/contents/collection/view.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Test getSortableFields()', async () => {
extension: 'json',
format: 'json',
},
_thumbnailFieldNames: [],
fields: [
{ name: 'title', widget: 'string' },
{ name: 'id', widget: 'string' },
Expand Down
3 changes: 2 additions & 1 deletion src/lib/services/contents/draft/save.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ describe('Test getEntryAssetFolderPaths()', () => {
const currentSlug = 'foo';

const collectionBase = {
_type: /** @type {CollectionType} */ ('entry'),
name: 'blog',
folder: 'src/content/blog',
_type: /** @type {CollectionType} */ ('entry'),
_thumbnailFieldNames: [],
};

const i18nBaseConfig = {
Expand Down
34 changes: 31 additions & 3 deletions src/lib/services/contents/entry/assets.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getPathInfo } from '@sveltia/utils/file';
import { escapeRegExp } from '@sveltia/utils/string';
import { get } from 'svelte/store';
import {
allAssets,
Expand All @@ -18,14 +19,41 @@ import { getFieldConfig } from '$lib/services/contents/entry/fields';
export const getEntryThumbnail = async (collection, entry) => {
const {
_i18n: { defaultLocale },
_thumbnailFieldName,
_thumbnailFieldNames,
} = collection;

const { locales } = entry;
const { content } = locales[defaultLocale] ?? Object.values(locales)[0] ?? {};

if (content && _thumbnailFieldName) {
return getMediaFieldURL(content[_thumbnailFieldName], entry, { thumbnail: true });
if (!content) {
return undefined;
}

/** @type {FieldKeyPath[]} */
const keyPathList = _thumbnailFieldNames
.map((name) => {
// Support a wildcard in the key path, e.g. `images.*.src`
if (name.includes('*')) {
const regex = new RegExp(`^${escapeRegExp(name).replace('\\*', '.+')}$`);

return Object.keys(content).filter((keyPath) => regex.test(keyPath));
}

return name;
})
.flat(1);

// Cannot use `Promise.all` or `Promise.any` here because we need the first available URL
// eslint-disable-next-line no-restricted-syntax
for (const keyPath of keyPathList) {
const url = content[keyPath]
? // eslint-disable-next-line no-await-in-loop
await getMediaFieldURL(content[keyPath], entry, { thumbnail: true })
: undefined;

if (url) {
return url;
}
}

return undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/services/contents/entry/slug.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ describe('Test fillSlugTemplate()', async () => {
/** @type {Collection} */
const collection = {
name: 'posts',
slug_length: 50,
_type: 'entry',
_file: {
extension: 'md',
format: 'yaml-frontmatter',
basePath: 'content/posts',
},
_i18n: defaultI18nConfig,
slug_length: 50,
_thumbnailFieldNames: [],
};

// @ts-ignore
Expand Down
3 changes: 2 additions & 1 deletion src/lib/services/contents/entry/summary.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('Test getEntrySummary()', () => {
const collection = {
name: 'pages-tags',
folder: 'content/tags',
slug_length: 50,
_type: 'entry',
_file: {
extension: 'md',
Expand All @@ -20,7 +21,7 @@ describe('Test getEntrySummary()', () => {
structure: 'multiple_files',
canonicalSlug: { key: 'translationKey', value: '{{slug}}' },
},
slug_length: 50,
_thumbnailFieldNames: [],
};

/** @type {LocalizedEntry} */
Expand Down
12 changes: 7 additions & 5 deletions src/lib/typedefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,11 @@
* @property {boolean} [divider] - A special option to make this collection a divider UI in the
* primary sidebar’s collection list. Other options will be ignored, but you may still need a random
* `name` and an empty `files` list to avoid a config file validation error in VS Code.
* @property {FieldKeyPath} [thumbnail] - Key path to an entry thumbnail displayed on the entry
* list. A nested field can be specified using dot notation, e.g. `images.0.src`. If omitted, the
* `name` of the first image field is used.
* @property {FieldKeyPath | FieldKeyPath[]} [thumbnail] - A field key path to be used to find an
* entry thumbnail displayed on the entry list. A nested field can be specified using dot notation,
* e.g. `heroImage.src`. A wildcard in the key path is also supported, e.g. `images.*.src`. Multiple
* key paths can be specified as an array for fallback purpose. If this option is omitted, the
* `name` of any non-nested, non-empty field using the Image or File widget is used.
* @see https://decapcms.org/docs/configuration-options/#collections
*/

Expand All @@ -469,8 +471,8 @@
* Extra properties for an entry collection.
* @typedef {object} EntryCollectionExtraProps
* @property {FileConfig} _file - Entry file configuration.
* @property {FieldKeyPath} [_thumbnailFieldName] - Key path to an entry thumbnail. The `thumbnail`
* option or the first image field name.
* @property {FieldKeyPath[]} _thumbnailFieldNames - A list of field key paths to be used to find an
* entry thumbnail. See {@link RawCollection.thumbnail} for details.
*/

/**
Expand Down

0 comments on commit 23648f6

Please sign in to comment.