Skip to content
Draft
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
d915617
feat(media dir nav): navigation through sub directories and breadcrumbs
andrew-paterson Dec 2, 2020
b712a7e
refactor: simplify code by listing files recursively
erezrokah Dec 6, 2020
1ce6f50
feat(media dir nav): uploading to nested directories works
andrew-paterson Dec 21, 2020
0c6fdef
feat(media dir nav): basic ui for multiple asset selection
andrew-paterson Dec 21, 2020
b6f6a81
feat(media dir nav): selected assets array cleared when user changes …
andrew-paterson Dec 21, 2020
4089afa
feat(media dir nav): delete button enabled when at least one media as…
andrew-paterson Dec 22, 2020
391aede
feat(media dir nav): synchronous deletion of multiple files
andrew-paterson Dec 22, 2020
a6499c9
feat(media dir nav): navigation through sub directories and breadcrumbs
andrew-paterson Dec 2, 2020
16195ef
refactor: simplify code by listing files recursively
erezrokah Dec 6, 2020
3c64681
feat(media dir nav): uploading to nested directories works
andrew-paterson Dec 21, 2020
405e04d
feat(media dir nav): basic ui for multiple asset selection
andrew-paterson Dec 21, 2020
49bc289
feat(media dir nav): selected assets array cleared when user changes …
andrew-paterson Dec 21, 2020
7b0bcc6
feat(media dir nav): delete button enabled when at least one media as…
andrew-paterson Dec 22, 2020
97709e3
feat(media dir nav): synchronous deletion of multiple files
andrew-paterson Dec 22, 2020
cada8c9
refactor: code cleanup
erezrokah Jan 12, 2021
cdd4842
feat(media dir nav): fixed merge conflicts
andrew-paterson Jan 18, 2021
bfec05d
feat(media dir nav): current media folder initially derived from file…
andrew-paterson Jan 18, 2021
efdc470
feat(media dir nav): nav with collections works and is optional
andrew-paterson Jan 18, 2021
7d26160
feat(media dir nav): disabling dir nav works for fields and collections
andrew-paterson Jan 18, 2021
8fe35ca
feat(media dir nav): current media navigation forgotten when user clo…
andrew-paterson Jan 18, 2021
318c0c0
chore: fix linting, formating, tests
erezrokah Jan 28, 2021
cde1467
Merge branch 'master' into feat/nested_media_dirs
erezrokah Jan 28, 2021
1a44004
Merge branch 'master' into feat/nested_media_dirs
erezrokah Feb 7, 2021
0247ceb
Merge branch 'master' into feat/nested_media_dirs
erezrokah Feb 7, 2021
0ca4286
Merge branch 'master' into feat/nested_media_dirs
erezrokah Feb 7, 2021
0a2ab86
Merge branch 'master' into feat/nested_media_dirs
erezrokah Feb 14, 2021
198c4b1
feat(media dir nav): empty folder creation
andrew-paterson Feb 15, 2021
eb41f0e
Update packages/netlify-cms-core/src/components/MediaLibrary/MediaLib…
andrew-paterson Feb 15, 2021
3b822c4
feat(media dir nav): remove junk file
andrew-paterson Feb 15, 2021
2aa5ee2
feat(media dir nav): revert dev-test config
andrew-paterson Feb 24, 2021
7adaff1
Merge branch 'master' into feat/nested_media_dirs
erezrokah Mar 11, 2021
88cdf88
style: run prettier
erezrokah Mar 11, 2021
af24db8
Merge branch 'master' into feat/nested_media_dirs
erezrokah Mar 14, 2021
7540d60
fix: config access
erezrokah Mar 14, 2021
9041de0
fix: add missing translation
erezrokah Mar 14, 2021
8546b30
feat(media dir nav): revert dev-test config
andrew-paterson Mar 17, 2021
23ff683
Merge branch 'feat/nested_media_dirs' of github.com:andrew-paterson/n…
andrew-paterson Mar 17, 2021
f07937f
feat(media dir nav): fix breadcrumbs bug
andrew-paterson Mar 23, 2021
39a2e39
feat(media dir nav): use current media folder for select media file p…
andrew-paterson Mar 31, 2021
b8b2367
feat(media dir nav): accommodate collection and field level media fol…
andrew-paterson Apr 6, 2021
e0452c8
Merge branch 'master' into feat/nested_media_dirs
erezrokah Apr 6, 2021
e462272
chore: fix formatting and lint warnings
erezrokah Apr 6, 2021
9c3419e
Merge branch 'master' into feat/nested_media_dirs
erezrokah Apr 7, 2021
ccdbf6f
Merge branch 'master' into feat/nested_media_dirs
erezrokah Apr 7, 2021
73de67d
fix: typetcript error
erezrokah Apr 7, 2021
84a3063
feat(media dir nav): bugfix - current media folder returned correctly
andrew-paterson Apr 8, 2021
7369c12
Merge branch 'feat/nested_media_dirs' of github.com:andrew-paterson/n…
andrew-paterson Apr 8, 2021
23bc33b
feat(media dir nav): media files filtered in new way
andrew-paterson Apr 12, 2021
d04ef68
feat(media dir nav): remove console statement
andrew-paterson Apr 12, 2021
f31cdcd
fix: format and lint errors
erezrokah Apr 18, 2021
d2ef185
Merge branch 'feat/nested_media_dirs' of github.com:andrew-paterson/n…
andrew-paterson Apr 19, 2021
ab1dab8
feat(media dir nav): derive current media folder in entries module
andrew-paterson Apr 19, 2021
11ee881
feat(media dir nav): bugfix
Apr 20, 2021
7a27a65
feat(media dir nav): bugfix
Apr 20, 2021
f9ce92e
feat(media dir nav): small refactor to fix failing tests.
Apr 21, 2021
da3ff3e
feat(media dir nav): handle relative media_folder set on field
andrew-paterson Apr 26, 2021
25d49a3
Merge branch 'master' into feat/nested_media_dirs
erezrokah May 3, 2021
ddf8b98
Merge branch 'master' into feat/nested_media_dirs
erezrokah May 9, 2021
bcb07a1
Merge branch 'master' into feat/nested_media_dirs
erezrokah May 30, 2021
10fe4f2
style: run prettier
erezrokah May 30, 2021
0eb6f36
Merge branch 'main' into feat/nested_media_dirs
martinjagodic Aug 29, 2024
d3736d5
fix: css lint
martinjagodic Aug 29, 2024
fbee623
fix: js lint
martinjagodic Aug 29, 2024
4c24009
Merge branch 'main' into feat/nested_media_dirs
martinjagodic Nov 11, 2025
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
17 changes: 12 additions & 5 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
backend:
name: test-repo
name: github
repo: andrew-paterson/netlify-cms-hugo-test-site

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should revert changes to this file 😉

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


site_url: https://example.com

publish_mode: editorial_workflow
media_folder: assets/uploads
media_folder: static/images

collections: # A list of collections the CMS should be able to edit
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
Expand All @@ -13,7 +14,8 @@ collections: # A list of collections the CMS should be able to edit
description: >
The description is a great place for tone setting, high level information, and editing
guidelines that are specific to a collection.
folder: '_posts'
folder: 'content/posts'
media_folder: '/static/images/one'
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
create: true # Allow users to create new documents in this collection
Expand Down Expand Up @@ -44,14 +46,19 @@ collections: # A list of collections the CMS should be able to edit
time_format: 'HH:mm',
format: 'YYYY-MM-DD HH:mm',
}
- label: 'Other Image'
name: 'other'
widget: 'image'
required: false
tagname: ''
- label: 'Cover Image'
name: 'image'
widget: 'image'
required: false
tagname: ''

media_folder: '/static/images/one/one-deeper'
disable_media_folder_navigation: true
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }

- name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'FAQ' # Used in the UI
folder: '_faqs'
Expand Down
28 changes: 19 additions & 9 deletions packages/netlify-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ export interface Config {
cmsLabelPrefix: string;
}

export enum TreeFileType {
TREE = 'tree',
BLOB = 'blob',
}

interface TreeFile {
type: 'blob' | 'tree';
type: TreeFileType;
sha: string;
path: string;
raw?: string;
Expand Down Expand Up @@ -375,7 +380,9 @@ export default class API {
this.request(`${this.repoURL}/git/trees`, {
method: 'POST',
body: JSON.stringify({
tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }],
tree: [
{ path: 'README.md', mode: '100644', type: TreeFileType.BLOB, sha: item.sha },
],
}),
}),
)
Expand Down Expand Up @@ -653,8 +660,8 @@ export default class API {

async listFiles(
path: string,
{ repoURL = this.repoURL, branch = this.branch, depth = 1 } = {},
): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> {
{ repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {},
): Promise<{ type: TreeFileType; id: string; name: string; path: string; size: number }[]> {
const folder = trim(path, '/');
try {
const result: Octokit.GitGetTreeResponse = await this.request(
Expand All @@ -668,9 +675,12 @@ export default class API {
return (
result.tree
// filter only files and up to the required depth
.filter(file => file.type === 'blob' && file.path.split('/').length <= depth)
.filter(
file =>
types.includes(file.type as TreeFileType) && file.path.split('/').length <= depth,
)
.map(file => ({
type: file.type,
type: file.type as TreeFileType,
id: file.sha,
name: basename(file.path),
path: `${folder}/${file.path}`,
Expand Down Expand Up @@ -1373,7 +1383,7 @@ export default class API {
const entry = {
path: trimStart(file.path, '/'),
mode: '100644',
type: 'blob',
type: TreeFileType.BLOB,
sha: file.sha,
} as TreeEntry;

Expand All @@ -1395,14 +1405,14 @@ export default class API {
tree.push({
path: file.path,
mode: '100644',
type: 'blob',
type: TreeFileType.BLOB,
sha: null,
});
// create in new path
tree.push({
path: file.path.replace(sourceDir, destDir),
mode: '100644',
type: 'blob',
type: TreeFileType.BLOB,
sha: file.path === from ? sha : file.id,
});
}
Expand Down
50 changes: 31 additions & 19 deletions packages/netlify-cms-backend-github/src/GraphQLAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import {
} from 'netlify-cms-lib-util';
import { trim, trimStart } from 'lodash';
import introspectionQueryResultData from './fragmentTypes';
import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API';
import API, {
Config,
BlobArgs,
API_NAME,
PullRequestState,
MOCK_PULL_REQUEST,
TreeFileType,
} from './API';
import * as queries from './queries';
import * as mutations from './mutations';
import { GraphQLError } from 'graphql';
Expand All @@ -35,7 +42,7 @@ interface TreeEntry {
object?: {
entries: TreeEntry[];
};
type: 'blob' | 'tree';
type: TreeFileType;
name: string;
sha: string;
blob?: {
Expand All @@ -47,7 +54,7 @@ interface TreeFile {
path: string;
id: string;
size: number;
type: string;
type: TreeFileType;
name: string;
}

Expand Down Expand Up @@ -337,30 +344,35 @@ export default class GraphQLAPI extends API {
}
}

getAllFiles(entries: TreeEntry[], path: string) {
getAllFiles(entries: TreeEntry[], path: string, types: TreeFileType[]) {
const allFiles: TreeFile[] = entries.reduce((acc, item) => {
if (item.type === 'tree') {
const itemToAdd = {
name: item.name,
type: item.type,
id: item.sha,
path: `${path}/${item.name}`,
size: item.blob ? item.blob.size : 0,
};
if (item.type === TreeFileType.BLOB) {
return [...acc, itemToAdd];
}

if (item.type === TreeFileType.TREE) {
const entries = item.object?.entries || [];
return [...acc, ...this.getAllFiles(entries, `${path}/${item.name}`)];
} else if (item.type === 'blob') {
return [
...acc,
{
name: item.name,
type: item.type,
id: item.sha,
path: `${path}/${item.name}`,
size: item.blob ? item.blob.size : 0,
},
];
const currentDir = types.includes(TreeFileType.TREE) ? [itemToAdd] : [];
const subFiles = this.getAllFiles(entries, `${path}/${item.name}`, types);
return [...acc, ...currentDir, ...subFiles];
}

return acc;
}, [] as TreeFile[]);
return allFiles;
}

async listFiles(path: string, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}) {
async listFiles(
path: string,
{ repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {},
) {
const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
const folder = trim(path, '/');
const { data } = await this.query({
Expand All @@ -369,7 +381,7 @@ export default class GraphQLAPI extends API {
});

if (data.repository.object) {
const allFiles = this.getAllFiles(data.repository.object.entries, folder);
const allFiles = this.getAllFiles(data.repository.object.entries, folder, types);
return allFiles;
} else {
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('github GraphQL API', () => {
];
const path = 'posts';

expect(api.getAllFiles(entries, path)).toEqual([
expect(api.getAllFiles(entries, path, ['blob'])).toEqual([
{
name: 'post-1.md',
id: 'sha-1',
Expand All @@ -65,5 +65,70 @@ describe('github GraphQL API', () => {
},
]);
});

it('should should return directories when types includes `tree`', () => {
const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' });
const entries = [
{
name: 'post-1.md',
sha: 'sha-1',
type: 'blob',
blob: { size: 1 },
},
{
name: 'post-2.md',
sha: 'sha-2',
type: 'blob',
blob: { size: 2 },
},
{
name: '2019',
sha: 'dir-sha',
type: 'tree',
object: {
entries: [
{
name: 'nested-post.md',
sha: 'nested-post-sha',
type: 'blob',
blob: { size: 3 },
},
],
},
},
];
const path = 'posts';

expect(api.getAllFiles(entries, path, ['blob', 'tree'])).toEqual([
{
name: 'post-1.md',
id: 'sha-1',
type: 'blob',
size: 1,
path: 'posts/post-1.md',
},
{
name: 'post-2.md',
id: 'sha-2',
type: 'blob',
size: 2,
path: 'posts/post-2.md',
},
{
name: '2019',
id: 'dir-sha',
type: 'tree',
size: 0,
path: 'posts/2019',
},
{
name: 'nested-post.md',
id: 'nested-post-sha',
type: 'blob',
size: 3,
path: 'posts/2019/nested-post.md',
},
]);
});
});
});
29 changes: 20 additions & 9 deletions packages/netlify-cms-backend-github/src/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
} from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage';
import { Octokit } from '@octokit/rest';
import API, { API_NAME } from './API';
import API, { API_NAME, TreeFileType } from './API';
import GraphQLAPI from './GraphQLAPI';

type GitHubUser = Octokit.UsersGetAuthenticatedResponse;
Expand Down Expand Up @@ -436,14 +436,25 @@ export default class GitHub implements Implementation {
.catch(() => ({ file: { path, id: null }, data: '' }));
}

getMedia(mediaFolder = this.mediaFolder) {
return this.api!.listFiles(mediaFolder).then(files =>
files.map(({ id, name, size, path }) => {
// load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls
// for private repositories
return { id, name, size, displayURL: { id, path }, path };
}),
);
async getMedia(mediaFolder = this.mediaFolder) {
const files = await this.api!.listFiles(mediaFolder, {
types: [TreeFileType.BLOB, TreeFileType.TREE],
depth: 100,
});
const withDisplayUrls = files.map(({ id, name, size, path, type }) => {
// load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls
// for private repositories
return {
id,
name,
size,
displayURL: { id, path },
path,
isDirectory: type === TreeFileType.TREE,
hasChildren: files.filter(file => file.path.startsWith(path)).length > 1,
};
});
return withDisplayUrls;
}

async getMediaFile(path: string) {
Expand Down
25 changes: 21 additions & 4 deletions packages/netlify-cms-core/src/actions/mediaLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,24 @@ function createMediaFileFromAsset({
}

export function persistMedia(file: File, opts: MediaOptions = {}) {
const { privateUpload, field } = opts;
const { privateUpload, field, currentMediaFolder } = opts;
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
const integration = selectIntegration(state, null, 'assetStore');
const files: MediaFile[] = selectMediaFiles(state, field);
const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug'));
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);

const entry = state.entryDraft.get('entry');
const collection = state.collections.get(entry?.get('collection'));
const path = selectMediaFilePath(
state.config,
collection,
entry,
fileName,
field,
currentMediaFolder,
);
const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path);
const editingDraft = selectEditingDraft(state.entryDraft);

/**
Expand Down Expand Up @@ -263,7 +272,14 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
} else {
const entry = state.entryDraft.get('entry');
const collection = state.collections.get(entry?.get('collection'));
const path = selectMediaFilePath(state.config, collection, entry, fileName, field);
const path = selectMediaFilePath(
state.config,
collection,
entry,
fileName,
field,
currentMediaFolder,
);
assetProxy = createAssetProxy({
file,
path,
Expand Down Expand Up @@ -413,6 +429,7 @@ export function mediaLoading(page: number) {
interface MediaOptions {
privateUpload?: boolean;
field?: EntryField;
currentMediaFolder?: string;
}

export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
Expand Down
Loading