diff --git a/README.md b/README.md index b3253ed0..c1062709 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ npm run start Upon installation, configure the app using an API key generated via the imgix [Dashboard](https://dashboard.imgix.com/api-keys). **Ensure that the generated key has the following permissions: `Sources` and `Asset Manager Browse`.** +> [!TIP] +> You can also optionally configure a default Source ID for the app to use. When configured, you'll have to click the Sources dropdown to select an asset from a different source. + Following the instructions on the screen, enter in the API key and press `Verify`. If the key is valid, you will receive a notification that the key has been successfully verified. If verification fails, you will need to ensure that the key was entered correctly. @@ -382,4 +385,3 @@ export const query = graphql` } `; ``` - diff --git a/src/components/ConfigScreen/ConfigScreen.tsx b/src/components/ConfigScreen/ConfigScreen.tsx index 8d4a8159..03accf26 100644 --- a/src/components/ConfigScreen/ConfigScreen.tsx +++ b/src/components/ConfigScreen/ConfigScreen.tsx @@ -6,9 +6,7 @@ import { Workbench, Paragraph, TextField, - Notification, Icon, - Button, TextLink, List, ListItem, @@ -16,13 +14,13 @@ import { Subheading, } from '@contentful/forma-36-react-components'; import ImgixAPI, { APIError } from 'imgix-management-js'; -import debounce from 'lodash.debounce'; import './ConfigScreen.css'; import packageJson from './../../../package.json'; export interface AppInstallationParameters { imgixAPIKey?: string; + sourceID?: string; successfullyVerified?: boolean; } @@ -144,17 +142,44 @@ export default class Config extends Component { } onConfigure = async () => { + this.setState({ + ...this.state, + validationMessage: '', + parameters: { + ...this.state.parameters, + successfullyVerified: false, + }, + }); // This method will be called when a user clicks on "Install" // or "Save" in the configuration screen. // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook // ensure the API key is validated - await this.verifyAPIKey(); + const hasValidAPIKey = await this.verifyAPIKey(); + + if (!hasValidAPIKey) { + const validationMessage = + "We couldn't verify this API Key. Confirm your details and try again."; + this.setState({ + ...this.state, + validationMessage, + }); + + return false; // return false so we don't save invalid config details + } // Generate a new target state with the App assigned to the selected // content types const targetState = await this.createTargetState(); + this.setState({ + ...this.state, + parameters: { + ...this.state.parameters, + successfullyVerified: true, + }, + }); + return { // Parameters to be persisted as the app configuration. parameters: this.state.parameters, @@ -197,16 +222,36 @@ export default class Config extends Component { return { ...currentState, EditorInterface }; }; - handleChange = (e: ChangeEvent) => { + handleAPIKeyChange = ( + e: ChangeEvent, + ) => { + const prevState = { ...this.state }; this.setState({ + ...prevState, + validationMessage: '', parameters: { + ...prevState.parameters, imgixAPIKey: e.target.value, successfullyVerified: this.state.parameters.successfullyVerified, }, }); }; - verifyAPIKey = async () => { + handleSourceIDChange = ( + e: ChangeEvent, + ) => { + const prevState = { ...this.state }; + this.setState({ + ...prevState, + parameters: { + ...prevState.parameters, + sourceID: e.target.value, + successfullyVerified: this.state.parameters.successfullyVerified, + }, + }); + }; + + verifyAPIKey = async (): Promise => { this.setState({ isButtonLoading: true }); const imgix = new ImgixAPI({ @@ -214,21 +259,18 @@ export default class Config extends Component { pluginOrigin: `contentful/v${packageJson.version}`, }); - let updatedInstallationParameters: AppInstallationParameters = { - ...this.state.parameters, - }; - try { - await imgix.request('sources'); - Notification.setPosition('top', { offset: 650 }); - Notification.success( - 'Your API key was successfully confirmed! Click the Install/Save button (in the top right corner) to complete installation.', - { - duration: 10000, - id: 'ix-config-notification', - }, - ); - updatedInstallationParameters.successfullyVerified = true; + if (this.state.parameters.sourceID?.length) { + await imgix.request(`sources/${this.state.parameters.sourceID}`); + } else { + await imgix.request('sources'); + } + this.setState({ + ...this.state, + isButtonLoading: false, + }); + + return true; } catch (error) { // APIError will emit more helpful data for debugging if (error instanceof APIError) { @@ -236,43 +278,15 @@ export default class Config extends Component { } else { console.error(error); } - Notification.setPosition('top', { offset: 650 }); - Notification.error( - "We couldn't verify this API Key. Confirm your details and try again.", - { - duration: 3000, - id: 'ix-config-notification', - }, - ); - updatedInstallationParameters.successfullyVerified = false; - } finally { this.setState({ - validationMessage: '', + ...this.state, isButtonLoading: false, - parameters: updatedInstallationParameters, }); - } - }; - onClick = async () => { - if (this.state.parameters.imgixAPIKey === '') { - let updatedInstallationParameters: AppInstallationParameters = { - ...this.state.parameters, - }; - updatedInstallationParameters.successfullyVerified = false; - this.setState({ - validationMessage: 'Please input your API Key', - parameters: updatedInstallationParameters, - }); - } else { - await this.verifyAPIKey(); + return false; } }; - debounceOnClick = debounce(this.onClick, 1000, { - leading: true, - }); - getAPIKey = async () => { return this.props.sdk.app .getParameters() @@ -347,7 +361,7 @@ export default class Config extends Component { type: 'password', autoComplete: 'new-api-key', }} - onChange={this.handleChange} + onChange={this.handleAPIKeyChange} /> {this.state.parameters.successfullyVerified && ( @@ -366,16 +380,22 @@ export default class Config extends Component { https://dashboard.imgix.com/api-keys

+
+
+ +
+
- {this.state.contentTypes.length > 0 && (

diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx index c59756b3..c8908689 100644 --- a/src/components/Dialog/Dialog.tsx +++ b/src/components/Dialog/Dialog.tsx @@ -250,15 +250,21 @@ export default class Dialog extends Component { if (sources.length === 0) { throw noSourcesError(); } + const invocationParams = this.props.sdk.parameters + .invocation as AppInvocationParameters; + const installationParams = this.props.sdk.parameters + .installation as AppInstallationParameters; + // check if previously selected source exists // if it does, add it to state. - const previouslySelectedSource = ( - this.props.sdk.parameters.invocation as AppInvocationParameters - )?.selectedImage?.selectedSource; + const previouslySelectedSourceID = + invocationParams?.selectedImage?.selectedSource?.id || + installationParams.sourceID; const selectedSource = sources.find((source: any) => { - return source.id === previouslySelectedSource?.id; + return source.id === previouslySelectedSourceID; }); + if (selectedSource) { this.setState({ allSources: sources, selectedSource }); } else { diff --git a/src/components/Gallery/ImageGallery.tsx b/src/components/Gallery/ImageGallery.tsx index 6d91ccaa..3c42efd1 100644 --- a/src/components/Gallery/ImageGallery.tsx +++ b/src/components/Gallery/ImageGallery.tsx @@ -108,7 +108,12 @@ export class Gallery extends Component { ) : // If the source is a webfolder this.props.selectedSource.type === 'webfolder' ? ( diff --git a/src/components/SourceSelect/SourceSelectDropdown.tsx b/src/components/SourceSelect/SourceSelectDropdown.tsx index acfd50d7..8eff87c9 100644 --- a/src/components/SourceSelect/SourceSelectDropdown.tsx +++ b/src/components/SourceSelect/SourceSelectDropdown.tsx @@ -5,7 +5,6 @@ import { Dropdown, DropdownList, DropdownListItem, - Paragraph, Spinner, } from '@contentful/forma-36-react-components'; @@ -42,24 +41,30 @@ export function SourceSelectDropdown({ isOpen={isOpen} onClose={() => setOpen(false)} toggleElement={ - + !allSources.length ? ( + + ) : ( + + ) } > - {!allSources.length ? ( - - Loading - - ) : null} {allSources.map((source: SourceProps) => ( handleClick(source)}> {source.name}