Skip to content

Add option for FairData-based media store - #801

Merged
ajolipa merged 9 commits into
developfrom
rb/fairdata-media-store
Oct 28, 2026
Merged

ajolipa merged 9 commits into
developfrom
rb/fairdata-media-store

Conversation

@ajolipa

@ajolipa ajolipa commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

In this PR

Adds the ability to configure (via environment variable, for now) the Tina media store to connect directly to the associated FairData project(s) rather than a separate S3 backend. This option makes the media store read-only on the Tina side -- all upload, deletion etc. is done via FairData. As such, I also added some extra UI tinkering to disable the Upload, New Folder, and Delete buttons when using this option. (This disabling is cosmetic, but the custom media store has no-op methods for deletion and upload in any case.)
image

This works in tandem with the more feature-rich FairData record (and media) MDX inserts, allowing FairData media records to be used as thumbnails, inserted into rich text fields just as plain old images rather than IIIF viewers, and used in the layout of custom pages.

@netlify

netlify Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for marronage-staging ready!

Name Link
🔨 Latest commit 44dbf5e
🔍 Latest deploy log https://app.netlify.com/projects/marronage-staging/deploys/6ae2456c6bda955886cf1a28
😎 Deploy Preview https://deploy-preview-801--marronage-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for pss-scavenger-hunt ready!

Name Link
🔨 Latest commit 44dbf5e
🔍 Latest deploy log https://app.netlify.com/projects/pss-scavenger-hunt/deploys/6ae2456cbd98ec7a4f58f317
😎 Deploy Preview https://deploy-preview-801--pss-scavenger-hunt.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for padp-staging ready!

Name Link
🔨 Latest commit 44dbf5e
🔍 Latest deploy log https://app.netlify.com/projects/padp-staging/deploys/6ae2456cf427c629a81b0e8b
😎 Deploy Preview https://deploy-preview-801--padp-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for relnet-staging ready!

Name Link
🔨 Latest commit 44dbf5e
🔍 Latest deploy log https://app.netlify.com/projects/relnet-staging/deploys/6ae2456cba797cac94e25571
😎 Deploy Preview https://deploy-preview-801--relnet-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ajolipa
ajolipa requested a review from blms September 15, 2026 05:14

@blms blms left a comment •

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.

I don't see this anywhere in published docs, but it looks like the MediaStore interface actually has a read-only option already called isStatic:

https://github.com/tinacms/tinacms/blob/4a578f972431034ca682ae3446a8e74c4f60b7c4/packages/tinacms/src/toolkit/core/media.ts#L107-L112

  /**
   * Indicates that uploads and deletions are not supported
   *
   * @default false
   */
  isStatic?: boolean;

I wonder if you just add isStatic = true if that would eliminate the need for media-ui.ts?

I could see it being a bit brittle to rely on an undocumented option—maybe we should add a unit test to make sure that this option is still supported and does what we expect, so we can catch it when we upgrade Tina in the future. To me this seems less brittle than removing buttons by label.

@ajolipa

ajolipa commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

I don't see this anywhere in published docs, but it looks like the MediaStore interface actually has a read-only option already called isStatic

Good catch! Yeah I dislike the "try to find the right buttons by searching the DOM structure in a somewhat ad-hoc way and disable them" approach haha, but did not realize that the isStatic option existed. I have now updated it and it seems to work beautifully -- the irrelevant buttons are just absent from the UI. I added a test to make sure those buttons are absent in the case that the FD media store is configured.

@ajolipa
ajolipa requested a review from blms September 15, 2026 20:10
Comment thread test/browser/rbac.test.ts Resolved
Comment thread tina/config.ts Resolved
Comment thread tina/fairdata-media-store.ts Resolved
Comment thread tina/fairdata-media-store.ts Resolved
Comment thread test/browser/rbac.test.ts Outdated
});

test('Media manager hides folder/upload/delete controls for the FairData store', async ({ page }) => {
test.skip(process.env.TINA_PUBLIC_USE_FD_MEDIA !== 'true', 'FairData media store is not enabled');

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.

Re: this test.skip rule, I remember generally we were trying to avoid allowing the dev's local .env to change test results. Should we set this true here instead?

env: {
// Stop astro auto-daemonizing when it detects an agent shell (CLAUDECODE).
ASTRO_DEV_BACKGROUND: '1',
// Dev-role override in tina/utils/getUserRole.ts (needs IS_LOCAL=true).
TINA_PUBLIC_IS_LOCAL: 'true',
TINA_PUBLIC_DEV_ROLE: 'org:member',
TINA_PUBLIC_DEV_USER_ID: 'dev-user',
// Serve the committed fixtures instead of any real content repo, and
// never fetch (a dev's .env GITHUB_* would otherwise clone over them).
TINA_LOCAL_CONTENT_PATH: '../test/fixtures/',
SKIP_CONTENT_FETCH: 'true',
CONFIG_URL: '',
CONFIG_FILE: 'test/fixtures/content/settings/config.json'
}

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.

Now that this is hard coded in the RBAC playwright config, should we remove the skip here, and the TINA_PUBLIC_USE_FD_MEDIA: true in the github workflow?

@ajolipa
ajolipa requested a review from blms October 27, 2026 03:42
};
});

const totalPages = Number(response.list?.pages) || page;

@blms blms Oct 27, 2026 •

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.

Claude flagged that because this is running in the browser rather than server-side, and using MediaContentsService from @backend/api/coreData/mediaContents, that means it's using the JSON that omits list, so response.list will always be undefined here. It recommends adding the list field here, which is the server-side handling for the API response from FairData (which will have list) that actually is used to build the JSON:

async getAll(params: { [key: string] : any } = {}) {
let records;
if (this.useCache()) {
const entries = await getCollection(this.name);
records = _.map(entries, (entry) => entry.data);
} else {
const response = await this.service.fetchAll(_.defaults(params, REQUEST_PARAMS));
records = response[this.name];
}
return {
[this.name]: records
};
}

I think this should only hit the else block in that function here since media_contents has no content-collection loader, afaik, and so it could just store list from the fetchAll response on a local variable and return it alongside [this.name]: records if it's defined.

This will add a list field to every /api/<model>/index.json response, but I think that's reasonable/useful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh good catch, I'm not sure I fully realized that the /api routes don't return that data currently! I tested that it could still fetch the media with this paginated setup but I suppose it was just returning the first 100 and then stopping. 🤔

Comment thread test/browser/rbac.test.ts Outdated
});

test('Media manager hides folder/upload/delete controls for the FairData store', async ({ page }) => {
test.skip(process.env.TINA_PUBLIC_USE_FD_MEDIA !== 'true', 'FairData media store is not enabled');

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.

Now that this is hard coded in the RBAC playwright config, should we remove the skip here, and the TINA_PUBLIC_USE_FD_MEDIA: true in the github workflow?

Comment thread test/browser/rbac.test.ts
const firstMediaItem = page.locator('ul li button').first();
await expect(firstMediaItem).toBeVisible();
await firstMediaItem.click();
await expect(page.getByRole('button', { name: 'Delete', exact: true })).toHaveCount(0);

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.

I think this will be true (no delete button) between the click and when the item's detail panel opens, so it might not exactly be checking the contents of the panel. Maybe best if we wait for something we know will be inside of the panel first? e.g.

Suggested change
await expect(page.getByRole('button', { name: 'Delete', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Insert', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'Delete', exact: true })).toHaveCount(0);

@blms

blms commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Also just noticed the test failure in CI. It looks like it might be selecting the edit mode button? Might need to call await enterEditMode(page); at the start of the failing test to get past that. The selector for the firstMediaItem might also need to be updated to be more specific, something like ul li > button:has(img) should do the trick.

@ajolipa
ajolipa requested a review from blms October 28, 2026 19:08

@blms blms left a comment

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.

Looks great! 🚀

@ajolipa ajolipa added the next release Issues in the next release label Oct 28, 2026
@ajolipa
ajolipa merged commit 0b69af4 into develop Oct 28, 2026
20 checks passed
@ajolipa
ajolipa deleted the rb/fairdata-media-store branch October 28, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

next release Issues in the next release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants