Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add default sourceID configuration fields #223

Merged
merged 6 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- ix-docs-ignore -->
Expand Down Expand Up @@ -382,4 +385,3 @@ export const query = graphql`
}
`;
```

140 changes: 80 additions & 60 deletions src/components/ConfigScreen/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,21 @@ import {
Workbench,
Paragraph,
TextField,
Notification,
Icon,
Button,
TextLink,
List,
ListItem,
CheckboxField,
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;
}

Expand Down Expand Up @@ -144,17 +142,44 @@ export default class Config extends Component<ConfigProps, ConfigState> {
}

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,
Expand Down Expand Up @@ -197,82 +222,71 @@ export default class Config extends Component<ConfigProps, ConfigState> {
return { ...currentState, EditorInterface };
};

handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
handleAPIKeyChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
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<HTMLInputElement | HTMLTextAreaElement>,
) => {
const prevState = { ...this.state };
this.setState({
...prevState,
parameters: {
...prevState.parameters,
sourceID: e.target.value,
successfullyVerified: this.state.parameters.successfullyVerified,
},
});
};

verifyAPIKey = async (): Promise<boolean> => {
this.setState({ isButtonLoading: true });

const imgix = new ImgixAPI({
apiKey: this.state.parameters.imgixAPIKey || '',
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) {
console.error(error.toString());
} 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()
Expand Down Expand Up @@ -347,7 +361,7 @@ export default class Config extends Component<ConfigProps, ConfigState> {
type: 'password',
autoComplete: 'new-api-key',
}}
onChange={this.handleChange}
onChange={this.handleAPIKeyChange}
/>
</div>
{this.state.parameters.successfullyVerified && (
Expand All @@ -366,16 +380,22 @@ export default class Config extends Component<ConfigProps, ConfigState> {
https://dashboard.imgix.com/api-keys
</a>
</p>
<div className="flex-container">
<div className="flex-child">
<TextField
name="Default Source"
id="SourceID"
labelText="Default Source ID (Optional) "
value={this.state.parameters?.sourceID || ''}
textInputProps={{
type: 'text',
autoComplete: 'default-source-id',
}}
onChange={this.handleSourceIDChange}
/>
</div>
</div>
</div>
<Button
type="submit"
buttonType="positive"
disabled={!this.state.parameters.imgixAPIKey?.length}
loading={this.state.isButtonLoading}
onClick={this.debounceOnClick}
>
Verify
</Button>
{this.state.contentTypes.length > 0 && (
<div>
<hr></hr>
Expand Down
14 changes: 10 additions & 4 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,21 @@ export default class Dialog extends Component<DialogProps, DialogState> {
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 {
Expand Down
7 changes: 6 additions & 1 deletion src/components/Gallery/ImageGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ export class Gallery extends Component<GalleryProps, GalleryState> {
<GalleryPlaceholder
sdk={this.props.sdk}
handleClose={this.handleClose}
text="Select a Source to view your image gallery"
text={
// @ts-ignore
this.props.sdk.parameters.installation.sourceID
? 'Loading'
: 'Select a Source to view your image gallery'
}
/>
) : // If the source is a webfolder
this.props.selectedSource.type === 'webfolder' ? (
Expand Down
37 changes: 21 additions & 16 deletions src/components/SourceSelect/SourceSelectDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Dropdown,
DropdownList,
DropdownListItem,
Paragraph,
Spinner,
} from '@contentful/forma-36-react-components';

Expand Down Expand Up @@ -42,24 +41,30 @@ export function SourceSelectDropdown({
isOpen={isOpen}
onClose={() => setOpen(false)}
toggleElement={
<Button
size="small"
buttonType="muted"
className="ix-dropdown"
indicateDropdown
onClick={() => setOpen(!isOpen)}
disabled={disabled}
>
{selectedSource.name || 'Select an imgix Source'}
</Button>
!allSources.length ? (
<Button
size="small"
buttonType="muted"
className="ix-dropdown"
disabled={true}
>
<Spinner />
</Button>
) : (
<Button
size="small"
buttonType="muted"
className="ix-dropdown"
indicateDropdown
onClick={() => setOpen(!isOpen)}
disabled={disabled}
>
{selectedSource.name || 'Select an imgix Source'}
</Button>
)
}
>
<DropdownList className="ix-dropdown-list">
{!allSources.length ? (
<Paragraph style={{ paddingLeft: 5 }}>
Loading <Spinner />
</Paragraph>
) : null}
{allSources.map((source: SourceProps) => (
<DropdownListItem key={source.id} onClick={() => handleClick(source)}>
{source.name}
Expand Down