Was this page helpful?

Work with apps in Release context

This guide explains how to ensure your apps work correctly when entries are being edited within a release context, and how to implement opt-out mechanisms when your app is not compatible with releases.

Table of contents

Overview

Contentful's Timeline feature allows content editors to manage, preview, and schedule changes to a batch of entries. When an entry is being edited within a release context, some apps may encounter compatibility issues or need to adjust their behavior.

This guide provides practical solutions for handling release contexts in your apps, including opt-out mechanisms for apps that cannot function properly within releases.

NOTE: Apps in entry-related locations (Entry Field, Entry Sidebar, Entry Editor) are always affected by releases. Dialog apps inherit the release context when opened from entry-related locations but are unaffected when opened from other locations. Apps in Page, Home, and App Configuration locations are never affected by release contexts.

Understanding Release Context

When a content editor works with entries in a release, they're editing a release version of the entry that exists separately from the main entry. This has several implications for apps:

  • All entry data and references (including sdk.entry) are scoped to the release context, not the base entries
  • Some SDK methods like publish() and unpublish() are not available because the release itself manages the publishing state of all included entries
  • The sdk.release attribute contains information about the current release

Detecting Release Context

The primary way to detect whether your app is running in a release context is by checking the sdk.release attribute:

import { init } from '@contentful/app-sdk';

init((sdk) => {
  if (sdk.release) {
    // App is running in release context
    console.log('Release title:', sdk.release.title);
    console.log('Release start date:', sdk.release.start_date);
  } else {
    // App is running in normal (base entry) context
    console.log('App is running in base entry context');
  }
});

Release Object Structure

When in release context, sdk.release contains the following information:

{
  "sys": {
    "type": "Release",
    "id": "release-id",
    "version": 1,
    "space": {
      "sys": { "type": "Link", "linkType": "Space", "id": "space-id" }
    },
    "environment": {
      "sys": { "type": "Link", "linkType": "Environment", "id": "master" }
    },
    "createdAt": "2024-11-29T12:00:00.000Z",
    "updatedAt": "2024-11-29T12:00:00.000Z",
    "createdBy": { "sys": { "type": "Link", "linkType": "User", "id": "user-id" } },
    "updatedBy": { "sys": { "type": "Link", "linkType": "User", "id": "user-id" } }
  },
  "title": "Black Friday Campaign",
  "description": "Campaign for Black Friday promotion",
  "entities": {
    "sys": { "type": "Array" },
    "items": [
      {
        "sys": {
          "type": "Link",
          "linkType": "Entry", // or "Asset"
          "id": "entry-id"
        }
      }
    ]
  }
}

When not in release context, sdk.release will be undefined.

Opt-out patterns

There are several approaches you can take when your app encounters a release context:

Complete opt-out

The simplest approach is to render nothing when in release context:

import React from 'react';
import { useSDK } from '@contentful/react-apps-toolkit';

const Sidebar = () => {
  const sdk = useSDK();

  // Complete opt-out: render nothing in release context
  if (sdk.release) {
    return null;
  }

  // Normal app functionality for base entries
  return (
    <div>
      <h3>My App</h3>
      <p>This app works with base entries only.</p>
    </div>
  );
};

export default Sidebar;

Partial opt-out with fallback UI

Show a user-friendly message explaining why the app is not available:

import React from 'react';
import { Paragraph, Note } from '@contentful/f36-components';
import { useSDK } from '@contentful/react-apps-toolkit';

const FieldEditor = () => {
  const sdk = useSDK();

  // Partial opt-out: show explanatory message
  if (sdk.release) {
    return (
      <Note variant="warning" title="Not available in releases">
        <Paragraph>
          This app is not compatible with release entries. Please edit this
          field in the base entry or contact your administrator.
        </Paragraph>
      </Note>
    );
  }

  // Normal field editor functionality
  return <div>{/* Your field editor implementation */}</div>;
};

export default FieldEditor;

Conditional feature disabling

Disable specific features while keeping the app partially functional:

import React, { useState } from 'react';
import { Button, Stack, Paragraph } from '@contentful/f36-components';
import { useSDK } from '@contentful/react-apps-toolkit';

const EntryEditor = () => {
  const sdk = useSDK();
  const [isPublishing, setIsPublishing] = useState(false);

  const isInReleaseContext = !!sdk.release;

  const handlePublish = async () => {
    if (isInReleaseContext) {
      sdk.notifier.error('Publishing is not available in release context');
      return;
    }

    setIsPublishing(true);
    try {
      await sdk.entry.publish();
      sdk.notifier.success('Entry published successfully');
    } catch (error) {
      sdk.notifier.error('Failed to publish entry');
    } finally {
      setIsPublishing(false);
    }
  };

  return (
    <Stack flexDirection="column" spacing="spacingM">
      {isInReleaseContext && (
        <Paragraph>
          You are editing this entry in release context: "{sdk.release.title}"
        </Paragraph>
      )}

      <Button
        variant="primary"
        isDisabled={isInReleaseContext || isPublishing}
        isLoading={isPublishing}
        onClick={handlePublish}
      >
        {isInReleaseContext
          ? 'Publish (Not available in releases)'
          : 'Publish Entry'}
      </Button>

      {/* Other app functionality that works in both contexts */}
    </Stack>
  );
};

export default EntryEditor;

Affected App Locations

Apps in the following locations are affected by release contexts:

Location Constant Impact
Entry Field LOCATION_ENTRY_FIELD Field values are release-scoped
Entry Sidebar LOCATION_ENTRY_SIDEBAR Entry data is release-scoped
Entry Editor LOCATION_ENTRY_EDITOR Entire entry is release-scoped
Dialog LOCATION_DIALOG Inherits release context when opened from entry-related locations above

Apps in these locations are not affected by releases:

  • Page (LOCATION_PAGE)
  • Home (LOCATION_HOME)
  • App Configuration (LOCATION_APP_CONFIG)

Dialog Context Inheritance

When a dialog is opened from a release-aware location (Entry Field, Entry Sidebar, or Entry Editor), it inherits the parent's release context. This means:

  • sdk.release will be defined in the dialog app
  • Entity selector dialogs (selectSingleEntry, selectMultipleEntries, etc.) will show release-scoped entities
  • The dialog app should implement the same release-aware patterns as other entry-related apps

However, if the same dialog is opened from a non-release location (Page, Home, App Configuration), it will not have release context.

Unsupported Features in Release Context

The following SDK methods and features are not available when working with release-scoped entries:

Entry methods

  • sdk.entry.publish() - Throws error: "Publish entry method is not supported in release context"
  • sdk.entry.unpublish() - Throws error: "Unpublish entry method is not supported in release context"

Code examples

Entry field app with release awareness

import React, { useState, useEffect } from 'react';
import { TextInput, FormControl, HelpText } from '@contentful/f36-components';
import { useSDK } from '@contentful/react-apps-toolkit';

const CustomFieldEditor = () => {
  const sdk = useSDK();
  const [value, setValue] = useState(sdk.field.getValue() || '');

  // Handle release context
  if (sdk.release) {
    return (
      <FormControl>
        <FormControl.Label>Custom Field (Release Mode)</FormControl.Label>
        <TextInput
          value={value}
          onChange={(e) => {
            setValue(e.target.value);
            sdk.field.setValue(e.target.value);
          }}
        />
        <HelpText>Editing in release: {sdk.release.title}</HelpText>
      </FormControl>
    );
  }

  // Normal field editor
  return (
    <FormControl>
      <FormControl.Label>Custom Field</FormControl.Label>
      <TextInput
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
          sdk.field.setValue(e.target.value);
        }}
      />
    </FormControl>
  );
};

export default CustomFieldEditor;

Entry sidebar app with conditional features

import React from 'react';
import { Card, Paragraph, Button, Stack } from '@contentful/f36-components';
import { useSDK } from '@contentful/react-apps-toolkit';

const EntrySidebar = () => {
  const sdk = useSDK();

  const handleExternalSync = () => {
    if (sdk.release) {
      sdk.notifier.warning(
        'External sync is not available for release entries'
      );
      return;
    }
    // Perform external sync
  };

  return (
    <Card>
      <Stack flexDirection="column" spacing="spacingM">
        <Paragraph>
          {sdk.release ? `Release: ${sdk.release.title}` : 'Base Entry'}
        </Paragraph>

        <Button
          size="small"
          isDisabled={!!sdk.release}
          onClick={handleExternalSync}
        >
          Sync to External System
        </Button>

        {sdk.release && (
          <Paragraph marginTop="spacingS" fontColor="gray600">
            Some features are disabled in release context
          </Paragraph>
        )}
      </Stack>
    </Card>
  );
};

export default EntrySidebar;