diff --git a/packages/common/src/content/_internal/ContentSchema.ts b/packages/common/src/content/_internal/ContentSchema.ts index 323fc625cae..dfde2dd2bcc 100644 --- a/packages/common/src/content/_internal/ContentSchema.ts +++ b/packages/common/src/content/_internal/ContentSchema.ts @@ -24,5 +24,12 @@ export const ContentSchema: IConfigurationSchema = { schedule: { type: "object", }, + _forceUpdate: { + type: "array", + items: { + type: "boolean", + enum: [true], + }, + }, }, } as IConfigurationSchema; diff --git a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts index b91e6b5766a..da42bffdbc8 100644 --- a/packages/common/src/content/_internal/ContentUiSchemaSettings.ts +++ b/packages/common/src/content/_internal/ContentUiSchemaSettings.ts @@ -1,7 +1,7 @@ import { checkPermission } from "../.."; import { IArcGISContext } from "../../ArcGISContext"; import { EntityEditorOptions } from "../../core/schemas/internal/EditorOptions"; -import { IUiSchema } from "../../core/schemas/types"; +import { IUiSchema, UiSchemaRuleEffects } from "../../core/schemas/types"; import { IHubEditableContent } from "../../core/types/IHubEditableContent"; import { isHostedFeatureServiceEntity } from "../hostedServiceUtils"; @@ -45,13 +45,26 @@ export const buildUiSchema = async ( { type: "weekly" }, { type: "monthly" }, { type: "yearly" }, - // uncomment this when the manual option is available - // { - // label: `option.manual.label`, - // type: "manual", - // helperActionIcon: "information-f", - // helperActionText: "option.manual.helperActionText", - // }, + { + type: "manual", + helperActionIcon: "information-f", + helperActionText: `{{${i18nScope}.fields.schedule.manual.helperActionText:translate}}`, + }, + ], + }, + }, + // force update checkbox -- TODO: replace with button once available + { + type: "Control", + scope: "/properties/_forceUpdate", + options: { + control: "hub-field-input-tile-select", + type: "checkbox", + labels: [ + `{{${i18nScope}.fields.schedule.forceUpdateButton.label:translate}}`, + ], + descriptions: [ + `{{${i18nScope}.fields.schedule.forceUpdateButton.description:translate}}`, ], }, }, diff --git a/packages/common/src/content/_internal/getSchedulerApiUrl.ts b/packages/common/src/content/_internal/getSchedulerApiUrl.ts deleted file mode 100644 index 7919d1f515c..00000000000 --- a/packages/common/src/content/_internal/getSchedulerApiUrl.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IRequestOptions } from "@esri/arcgis-rest-request"; -import { getHubApiUrl } from "../../api"; - -export function getSchedulerApiUrl( - itemId: string, - requestOptions: IRequestOptions -): string { - // sometimes the url has /api/v3 at the end, so we need to remove it - const hubApiUrlWithVersion = getHubApiUrl(requestOptions); - const hubApiUrlRoot = hubApiUrlWithVersion.replace(/\/api\/v3$/, ""); - return `${hubApiUrlRoot}/api/download/v1/items/${itemId}/schedule`; -} diff --git a/packages/common/src/content/_internal/internalContentUtils.ts b/packages/common/src/content/_internal/internalContentUtils.ts index 89b771428a2..5a1b9987934 100644 --- a/packages/common/src/content/_internal/internalContentUtils.ts +++ b/packages/common/src/content/_internal/internalContentUtils.ts @@ -41,7 +41,8 @@ import { _getHubUrlFromPortalHostname } from "../../urls/_get-hub-url-from-porta import { IRequestOptions } from "@esri/arcgis-rest-request"; import { geojsonToArcGIS } from "@terraformer/arcgis"; import { Polygon } from "geojson"; -import { fetchItemEnrichments } from "../../items/_enrichments"; +import { getHubApiUrl } from "../../api"; +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; /** * Hashmap of Hub environment and application url surfix @@ -967,3 +968,35 @@ const getUrbanModelEditUrl = (item: IItem, requestOptions: IRequestOptions) => { const isPortalFromUrl = (portalUrl: string): boolean => { return portalUrl.indexOf("arcgis.com") === -1; }; + +export function getSchedulerApiUrl( + itemId: string, + requestOptions: IRequestOptions +): string { + const hubApiUrlRoot = getHubApiUrlRoot(requestOptions); + return `${hubApiUrlRoot}/api/download/v1/items/${itemId}/schedule`; +} + +export function getHubApiUrlRoot(requestOptions: IRequestOptions): string { + // sometimes the url has /api/v3 at the end, so we need to remove it + const hubApiUrlWithVersion = getHubApiUrl(requestOptions); + return hubApiUrlWithVersion.replace(/\/api\/v3$/, ""); +} + +export const forceUpdateContent = async ( + itemId: string, + requestOptions: IUserRequestOptions +) => { + const hubApiUrlRoot = getHubApiUrl(requestOptions); + const url = `${hubApiUrlRoot}/api/v3/jobs/item/${itemId}/harvest`; + const options = { + method: "POST", + headers: { + "content-type": "application/json", + authorization: requestOptions.authentication.token, + }, + }; + + const response = await fetch(url, options); + return response.ok; +}; diff --git a/packages/common/src/content/edit.ts b/packages/common/src/content/edit.ts index 0f01c990da6..bfee5cc7e1d 100644 --- a/packages/common/src/content/edit.ts +++ b/packages/common/src/content/edit.ts @@ -39,6 +39,8 @@ import { isDownloadSchedulingAvailable, maybeUpdateSchedule, } from "./manageSchedule"; +import { forceUpdateContent } from "./_internal/internalContentUtils"; +import { deepEqual } from "../objects"; // TODO: move this to defaults? const DEFAULT_CONTENT_MODEL: IModel = { @@ -174,6 +176,14 @@ export async function updateContent( } if (isDownloadSchedulingAvailable(requestOptions, content.access)) { + // if schedule has "Force Update" checked and clicked save, initiate an update + if (deepEqual(content._forceUpdate, [true])) { + // [true] + await forceUpdateContent(item.id, requestOptions); + } + + delete content._forceUpdate; + await maybeUpdateSchedule(content, requestOptions); } diff --git a/packages/common/src/content/manageSchedule.ts b/packages/common/src/content/manageSchedule.ts index 0aaff9fe259..e7c68103a2a 100644 --- a/packages/common/src/content/manageSchedule.ts +++ b/packages/common/src/content/manageSchedule.ts @@ -3,7 +3,7 @@ import { IHubSchedule, IHubScheduleResponse } from "../core/types/IHubSchedule"; import { cloneObject } from "../util"; import { deepEqual } from "../objects/deepEqual"; import { AccessLevel, IHubEditableContent } from "../core"; -import { getSchedulerApiUrl } from "./_internal/getSchedulerApiUrl"; +import { getSchedulerApiUrl } from "./_internal/internalContentUtils"; // Any code referencing these functions must first pass isDownloadSchedulingAvailable @@ -27,10 +27,20 @@ export const getSchedule = async ( } as IHubScheduleResponse; } + // if the schedule mode is set to manual, return it + if (schedule.mode === "manual") { + return { + schedule: { + mode: "manual", + }, + message: `Download schedule found for item ${itemId}`, + statusCode: 200, + } as IHubScheduleResponse; + } + // if the schedule is set, return it with added mode delete schedule.itemId; switch (schedule.cadence) { - // TODO: add manual option here when option is viable case "daily": case "weekly": case "monthly": @@ -58,7 +68,10 @@ export const setSchedule = async ( requestOptions: IRequestOptions ): Promise => { const body = cloneObject(schedule); - delete body.mode; + if (body.mode !== "manual") { + // remove mode if not manual + delete body.mode; + } const url = getSchedulerApiUrl(itemId, requestOptions); const options = { method: "POST", diff --git a/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts index fa7cbc18ae9..bfe27181176 100644 --- a/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts +++ b/packages/common/test/content/_internal/ContentUiSchemaSettings.test.ts @@ -43,13 +43,25 @@ describe("buildUiSchema: content settings", () => { { type: "weekly" }, { type: "monthly" }, { type: "yearly" }, - // uncomment this when the manual option is available - // { - // label: `option.manual.label`, - // type: "manual", - // helperActionIcon: "information-f", - // helperActionText: "option.manual.helperActionText", - // }, + { + type: "manual", + helperActionIcon: "information-f", + helperActionText: `{{some.scope.fields.schedule.manual.helperActionText:translate}}`, + }, + ], + }, + }, + { + type: "Control", + scope: "/properties/_forceUpdate", + options: { + control: "hub-field-input-tile-select", + type: "checkbox", + labels: [ + `{{some.scope.fields.schedule.forceUpdateButton.label:translate}}`, + ], + descriptions: [ + `{{some.scope.fields.schedule.forceUpdateButton.description:translate}}`, ], }, }, diff --git a/packages/common/test/content/edit.test.ts b/packages/common/test/content/edit.test.ts index 935115f0073..6e9ee264e2c 100644 --- a/packages/common/test/content/edit.test.ts +++ b/packages/common/test/content/edit.test.ts @@ -200,6 +200,8 @@ describe("content editing:", () => { // Since it already is, nothing should change serverExtractCapability: true, schedule: { mode: "automatic" }, + _forceUpdate: [], + access: "public", }; const chk = await updateContent(content, { ...MOCK_HUB_REQOPTS, @@ -245,6 +247,7 @@ describe("content editing:", () => { // Since it currently isn't, the service will be updated serverExtractCapability: true, schedule: { mode: "automatic" }, + _forceUpdate: [true], access: "public", }; const chk = await updateContent(content, { diff --git a/packages/common/test/content/manageSchedule.test.ts b/packages/common/test/content/manageSchedule.test.ts index 41d3c086c42..463c3970971 100644 --- a/packages/common/test/content/manageSchedule.test.ts +++ b/packages/common/test/content/manageSchedule.test.ts @@ -11,7 +11,7 @@ import { import { MOCK_HUB_REQOPTS } from "../mocks/mock-auth"; import { IHubEditableContent } from "../../src/core/types/IHubEditableContent"; import * as fetchMock from "fetch-mock"; -import { getSchedulerApiUrl } from "../../src/content/_internal/getSchedulerApiUrl"; +import { getSchedulerApiUrl } from "../../src/content/_internal/internalContentUtils"; describe("manageSchedule", () => { afterEach(() => { @@ -34,7 +34,6 @@ describe("manageSchedule", () => { "https://hubqa.arcgis.com/api/download/v1/items/123/schedule" ); }); - it("getSchedule: returns an error if no schedule is set", async () => { const item = { id: "123" }; fetchMock.once( @@ -54,8 +53,7 @@ describe("manageSchedule", () => { ); expect(fetchMock.calls().length).toBe(1); }); - - it("getSchedule: returns schedule if set", async () => { + it("getSchedule: returns schedule of mode 'scheduled' if set", async () => { const item = { id: "123" }; fetchMock.once( `https://hubqa.arcgis.com/api/download/v1/items/${item.id}/schedule`, @@ -63,6 +61,7 @@ describe("manageSchedule", () => { cadence: "daily", hour: 0, timezone: "America/New_York", + itemId: "123", } ); const response: IHubScheduleResponse = await getSchedule( @@ -77,8 +76,25 @@ describe("manageSchedule", () => { }); expect(fetchMock.calls().length).toBe(1); }); - - it("setSchedule: sets the item's schedule", async () => { + it("getSchedule: returns schedule of mode 'manual' if set", async () => { + const item = { id: "123" }; + fetchMock.once( + `https://hubqa.arcgis.com/api/download/v1/items/${item.id}/schedule`, + { + mode: "manual", + itemId: "123", + } + ); + const response: IHubScheduleResponse = await getSchedule( + item.id, + MOCK_HUB_REQOPTS + ); + expect(response.schedule).toEqual({ + mode: "manual", + }); + expect(fetchMock.calls().length).toBe(1); + }); + it("setSchedule: sets the item's schedule of mode 'scheduled'", async () => { const item = { id: "123" }; const schedule = { mode: "scheduled", @@ -98,7 +114,23 @@ describe("manageSchedule", () => { expect(response.message).toEqual("Download schedule set successfully."); expect(fetchMock.calls().length).toBe(1); }); + it("setSchedule: sets the item's schedule of mode 'manual'", async () => { + const item = { id: "123" }; + const schedule = { + mode: "manual", + } as IHubSchedule; + + fetchMock.post( + `https://hubqa.arcgis.com/api/download/v1/items/${item.id}/schedule`, + { + message: "Download schedule set successfully.", + } + ); + const response = await setSchedule(item.id, schedule, MOCK_HUB_REQOPTS); + expect(response.message).toEqual("Download schedule set successfully."); + expect(fetchMock.calls().length).toBe(1); + }); it("setSchedule: attempts to set an invalid schedule", async () => { const item = { id: "123" }; const schedule = { @@ -123,7 +155,6 @@ describe("manageSchedule", () => { ); expect(fetchMock.calls().length).toBe(1); }); - it("deleteSchedule: tries to delete an item's schedule", async () => { const item = { id: "123" }; @@ -138,7 +169,6 @@ describe("manageSchedule", () => { expect(response.message).toEqual("Download schedule deleted successfully."); expect(fetchMock.calls().length).toBe(1); }); - it("maybeUpdateSchedule: no schedule is set, and updating to automatic is not needed", async () => { const item = { id: "123" }; const content = { @@ -161,7 +191,6 @@ describe("manageSchedule", () => { ); expect(fetchMock.calls().length).toBe(1); }); - it("maybeUpdateSchedule: no schedule is set, and updating to scheduled is needed", async () => { const item = { id: "123" }; const content = { @@ -194,7 +223,6 @@ describe("manageSchedule", () => { expect(response.message).toEqual("Download schedule set successfully."); expect(fetchMock.calls().length).toBe(2); }); - it("maybeUpdateSchedule: schedule is set, and updating to automatic requires deleting the schedule", async () => { const item = { id: "123" }; const content = { @@ -222,7 +250,6 @@ describe("manageSchedule", () => { expect(response.message).toEqual("Download schedule deleted successfully."); expect(fetchMock.calls().length).toBe(2); }); - it("maybeUpdateSchedule: schedule is set, and no action is needed as schedules deepEqual each other", async () => { const item = { id: "123" }; const content = {