feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)
Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
33
ui/pro/AssetsPage.svelte
Normal file
33
ui/pro/AssetsPage.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {
|
||||
assetsSettings,
|
||||
assetsSettingsChanged,
|
||||
assetsSettingsLocked,
|
||||
currentAssetsSettings,
|
||||
enableAssets
|
||||
} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import AssetsSettings from "./AssetsSettings.svelte";
|
||||
import AssetsUpgrade from "./AssetsUpgrade.svelte";
|
||||
import Footer from "../components/Footer.svelte";
|
||||
import {setContext} from "svelte";
|
||||
|
||||
export let name = "assets";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", assetsSettingsLocked );
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent initialSettings={currentAssetsSettings}>
|
||||
<Notifications tab={name}/>
|
||||
<div class="assets-page wrapper">
|
||||
{#if $enableAssets}
|
||||
<AssetsSettings/>
|
||||
{:else}
|
||||
<AssetsUpgrade/>
|
||||
{/if}
|
||||
</div>
|
||||
</Page>
|
||||
|
||||
<Footer settingsStore={assetsSettings} settingsChangedStore={assetsSettingsChanged} on:routeEvent/>
|
||||
67
ui/pro/AssetsSettings.svelte
Normal file
67
ui/pro/AssetsSettings.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import {assetsSettings, assetsDefinedSettings} from "./stores";
|
||||
import AssetsSettingsHeaderRow from "./AssetsSettingsHeaderRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import SettingsPanelOption from "../components/SettingsPanelOption.svelte";
|
||||
import SettingsValidationStatusRow
|
||||
from "../components/SettingsValidationStatusRow.svelte";
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the provided domain name is invalid.
|
||||
*
|
||||
* @param {string} domain
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function validator( domain ) {
|
||||
const domainPattern = /[^a-z0-9.-]/;
|
||||
|
||||
let message = "";
|
||||
|
||||
if ( domain.trim().length === 0 ) {
|
||||
message = $strings.domain_blank;
|
||||
} else if ( true === domainPattern.test( domain ) ) {
|
||||
message = $strings.domain_invalid_content;
|
||||
} else if ( domain.length < 3 ) {
|
||||
message = $strings.domain_too_short;
|
||||
} else if ( domain === $urls.home_domain ) {
|
||||
message = $strings.assets_domain_same_as_site;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel name="settings" class="assets-panel" heading={$strings.assets_title} helpKey="assets-pull">
|
||||
<AssetsSettingsHeaderRow/>
|
||||
<SettingsValidationStatusRow section="assets"/>
|
||||
<SettingsPanelOption
|
||||
heading={$strings.assets_rewrite_urls}
|
||||
description={$strings.assets_rewrite_urls_desc}
|
||||
placeholder="assets.example.com"
|
||||
toggleName="rewrite-urls"
|
||||
bind:toggle={$assetsSettings["rewrite-urls"]}
|
||||
textName="domain"
|
||||
bind:text={$assetsSettings["domain"]}
|
||||
definedSettings={assetsDefinedSettings}
|
||||
{validator}
|
||||
>
|
||||
</SettingsPanelOption>
|
||||
|
||||
<SettingsPanelOption
|
||||
heading={$strings.assets_force_https}
|
||||
description={$strings.assets_force_https_desc}
|
||||
toggleName="force-https"
|
||||
bind:toggle={$assetsSettings["force-https"]}
|
||||
definedSettings={assetsDefinedSettings}
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
<div class="btn-row">
|
||||
<div class="notice">
|
||||
<img class="icon notice-icon assets-wizard" src="{$urls.assets + 'img/icon/assets-wizard.svg'}" alt="Launch the Assets Setup Wizard"/><a href={$urls.settings} class="link">Launch the Assets Setup Wizard</a>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
42
ui/pro/AssetsSettingsHeaderRow.svelte
Normal file
42
ui/pro/AssetsSettingsHeaderRow.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
</script>
|
||||
|
||||
|
||||
<PanelRow header class="assets">
|
||||
<img src="{$urls.assets + 'img/icon/assets.svg'}" alt="foo"/>
|
||||
<div class="assets-details">
|
||||
<h3>{$strings.assets_panel_header}</h3>
|
||||
<p class="console-details">
|
||||
{$strings.assets_panel_header_details}
|
||||
</p>
|
||||
</div>
|
||||
</PanelRow>
|
||||
|
||||
<style>
|
||||
:global(#as3cf-settings.wpome div.panel.settings .header) img {
|
||||
width: var(--as3cf-settings-ctrl-width);
|
||||
height: var(--as3cf-settings-ctrl-width);
|
||||
}
|
||||
|
||||
.assets-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
margin-left: var(--as3cf-settings-option-indent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .assets-details h3 {
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:global(#as3cf-settings.wpome div.panel) .console-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--as3cf-color-gray-600);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
37
ui/pro/AssetsUpgrade.svelte
Normal file
37
ui/pro/AssetsUpgrade.svelte
Normal file
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "../components/Upsell.svelte";
|
||||
|
||||
let benefits = [
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/fonts.svg',
|
||||
alt: 'js icon',
|
||||
text: $strings.assets_uppsell_benefits.js,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/css.svg',
|
||||
alt: 'css icon',
|
||||
text: $strings.assets_uppsell_benefits.css,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + 'img/icon/fonts.svg',
|
||||
alt: 'fonts icon',
|
||||
text: $strings.assets_uppsell_benefits.fonts,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Upsell benefits={benefits}>
|
||||
<div slot="heading">{$strings.assets_upsell_heading}</div>
|
||||
|
||||
<div slot="description">{@html $strings.assets_upsell_description}</div>
|
||||
|
||||
<a slot="call-to-action" href={$urls.upsell_discount_assets} class="button btn-lg btn-primary">
|
||||
<img src={$urls.assets + "img/icon/stars.svg"} alt="stars icon" style="margin-right: 5px;">
|
||||
{$strings.assets_upsell_cta}
|
||||
</a>
|
||||
|
||||
<div slot="call-to-action-note">
|
||||
{@html $strings.assets_upsell_cta_note}
|
||||
</div>
|
||||
</Upsell>
|
||||
62
ui/pro/CopyBucketsPromptSubPage.svelte
Normal file
62
ui/pro/CopyBucketsPromptSubPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {pop} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import SubPage from "../components/SubPage.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
|
||||
const tool = $tools.copy_buckets;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="copy-buckets" route="/storage/copy-buckets">
|
||||
<Panel
|
||||
heading={tool.title}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
skipText={$strings.no}
|
||||
nextText={$strings.yes}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</SubPage>
|
||||
13
ui/pro/DocumentationSidebar.svelte
Normal file
13
ui/pro/DocumentationSidebar.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import {strings} from "../js/stores";
|
||||
import {documentation} from "./stores";
|
||||
</script>
|
||||
|
||||
{#if $documentation.length}
|
||||
<div class="documentation">
|
||||
<h3>{$strings.documentation_title}</h3>
|
||||
{#each $documentation as item}
|
||||
<a href={item.url} class="link" target="_blank">{item.title}</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
62
ui/pro/DownloaderPromptPage.svelte
Normal file
62
ui/pro/DownloaderPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "downloader";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.downloader;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
26
ui/pro/Header.svelte
Normal file
26
ui/pro/Header.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script>
|
||||
import {push} from "svelte-spa-router";
|
||||
import {strings, urls} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import Header from "../components/Header.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
</script>
|
||||
|
||||
<Header>
|
||||
{#if $licence.is_set}
|
||||
{#if $licence.is_valid}
|
||||
<div class="licence-type">
|
||||
<img src={$urls.assets + "img/icon/licence-checked.svg"} alt={$strings.licence_checked}/>
|
||||
<a href={$urls.licenses} class="licence" target="_blank">{$licence.plan_plus_licence}</a>
|
||||
</div>
|
||||
<p>{@html $licence.customer}</p>
|
||||
{:else}
|
||||
<div class="licence-type">
|
||||
<img src={$urls.assets + "img/icon/error.svg"} alt={$strings.licence_error}/>
|
||||
<a href={$urls.licenses} class="licence" target="_blank">{$licence.status_description}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Button large primary on:click={() => push("/license")}>{$strings.activate_licence}</Button>
|
||||
{/if}
|
||||
</Header>
|
||||
103
ui/pro/LicencePage.svelte
Normal file
103
ui/pro/LicencePage.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<script>
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {api, config, settings, strings} from "../js/stores";
|
||||
import {autofocus} from "../js/autofocus";
|
||||
import {licence} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
import DefinedInWPConfig from "../components/DefinedInWPConfig.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let name = "licence";
|
||||
|
||||
let value = "";
|
||||
|
||||
/**
|
||||
* Handles an "Activate License" button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleActivateLicence( event ) {
|
||||
const result = await api.post( "licences", { licence: value } );
|
||||
|
||||
await updateLicenceInfo( result )
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a "Remove License" button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleRemoveLicence( event ) {
|
||||
value = "";
|
||||
const result = await api.delete( "licences" );
|
||||
|
||||
await updateLicenceInfo( result )
|
||||
}
|
||||
|
||||
/**
|
||||
* Update licence store with results of API call.
|
||||
*
|
||||
* @param {Object} response
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function updateLicenceInfo( response ) {
|
||||
if ( response.hasOwnProperty( "licences" ) ) {
|
||||
config.update( currentConfig => {
|
||||
return {
|
||||
...currentConfig,
|
||||
licences: response.licences
|
||||
};
|
||||
} );
|
||||
}
|
||||
|
||||
// Regardless of what just happened, make sure our settings are in sync (includes reference to license).
|
||||
await settings.fetch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name}/>
|
||||
<h2 class="page-title">{$strings.licence_title}</h2>
|
||||
|
||||
<div class="licence-page wrapper" class:defined={$licence.is_set && $licence.is_defined}>
|
||||
{#if $licence.is_set}
|
||||
<label for="licence-key" class="screen-reader-text">{$strings.licence_title}</label>
|
||||
<input
|
||||
id="licence-key"
|
||||
type="text"
|
||||
class="licence-field disabled"
|
||||
name="licence"
|
||||
value={$licence.masked_licence}
|
||||
disabled
|
||||
>
|
||||
{#if $licence.is_defined}
|
||||
<DefinedInWPConfig defined/>
|
||||
{:else}
|
||||
<Button large outline on:click={handleRemoveLicence}>{$strings.remove_licence}</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
<label for="enter-licence-key" class="screen-reader-text">{$strings.enter_licence_key}</label>
|
||||
<input
|
||||
id="enter-licence-key"
|
||||
type="text"
|
||||
class="licence-field"
|
||||
name="licence"
|
||||
minlength="4"
|
||||
placeholder={$strings.enter_licence_key}
|
||||
bind:value
|
||||
use:autofocus
|
||||
>
|
||||
<Button large primary on:click={handleActivateLicence} disabled={value.length === 0}>
|
||||
{$strings.activate_licence}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Page>
|
||||
92
ui/pro/MoveObjectsPromptPage.svelte
Normal file
92
ui/pro/MoveObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const moveObjectsTool = $tools.move_objects;
|
||||
const movePublicObjectsTool = $tools.move_public_objects;
|
||||
const movePrivateObjectsTool = $tools.move_private_objects;
|
||||
|
||||
let movePublicObjects = false;
|
||||
let movePrivateObjects = true;
|
||||
|
||||
$: nextDisabled = $settingsLocked || (!movePublicObjects && !movePrivateObjects);
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
let tool = moveObjectsTool;
|
||||
|
||||
if ( !movePublicObjects || !movePrivateObjects ) {
|
||||
tool = movePublicObjects ? movePublicObjectsTool : movePrivateObjectsTool;
|
||||
}
|
||||
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={movePublicObjectsTool.name}
|
||||
toggleName="move-public-objects"
|
||||
bind:toggle={movePublicObjects}
|
||||
helpURL={movePublicObjectsTool.doc_url}
|
||||
helpDesc={movePublicObjectsTool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html movePublicObjectsTool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="toggle-header"
|
||||
heading={movePrivateObjectsTool.name}
|
||||
toggleName="move-private-objects"
|
||||
bind:toggle={movePrivateObjects}
|
||||
helpURL={movePrivateObjectsTool.doc_url}
|
||||
helpDesc={movePrivateObjectsTool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html movePrivateObjectsTool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={moveObjectsTool.button}
|
||||
skipVisible={true}
|
||||
{nextDisabled}
|
||||
/>
|
||||
</Page>
|
||||
62
ui/pro/MovePrivateObjectsPromptPage.svelte
Normal file
62
ui/pro/MovePrivateObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-private-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.move_private_objects;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
62
ui/pro/MovePublicObjectsPromptPage.svelte
Normal file
62
ui/pro/MovePublicObjectsPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "move-public-objects";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.move_public_objects;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
109
ui/pro/Nav.svelte
Normal file
109
ui/pro/Nav.svelte
Normal file
@@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import {link} from "svelte-spa-router";
|
||||
import {bucket_writable, counts, strings, urls} from "../js/stores";
|
||||
import {licence, offloadRemainingWithCount, running, tools} from "./stores";
|
||||
import Nav from "../components/Nav.svelte";
|
||||
import OffloadStatus from "../components/OffloadStatus.svelte";
|
||||
import ToolRunningStatus from "./ToolRunningStatus.svelte";
|
||||
import OffloadStatusFlyout from "../components/OffloadStatusFlyout.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
let flyoutButton;
|
||||
let expanded = false;
|
||||
let hasFocus = false;
|
||||
|
||||
/**
|
||||
* Get a message describing why the offload remaining button is disabled, if it is.
|
||||
*
|
||||
* @param {Object} licence
|
||||
* @param {Object} counts
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getOffloadRemainingDisabledMessage( licence, counts ) {
|
||||
if ( !licence.is_set ) {
|
||||
return $strings.no_licence;
|
||||
}
|
||||
|
||||
if ( counts.total < 1 ) {
|
||||
return $strings.no_media;
|
||||
}
|
||||
|
||||
if ( counts.not_offloaded < 1 ) {
|
||||
return $strings.all_media_offloaded;
|
||||
}
|
||||
|
||||
if (
|
||||
licence.limit_info.counts_toward_limit &&
|
||||
licence.limit_info.total > 0 &&
|
||||
licence.limit_info.limit > 0 &&
|
||||
licence.limit_info.total >= licence.limit_info.limit
|
||||
) {
|
||||
if ( licence.limit_info.total > licence.limit_info.limit ) {
|
||||
return $strings.licence_limit_exceeded;
|
||||
}
|
||||
|
||||
return $strings.licence_limit_reached;
|
||||
}
|
||||
|
||||
if ( ! $bucket_writable ) {
|
||||
return $strings.disabled_tool_bucket_access;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$: offloadRemainingDisabledMessage = getOffloadRemainingDisabledMessage( $licence, $counts );
|
||||
|
||||
/**
|
||||
* Close the flyout panel and kick off the offloader.
|
||||
*
|
||||
* The panel is closed so that it does not pop back open without focus on completion.
|
||||
*/
|
||||
function startOffload() {
|
||||
expanded = false;
|
||||
tools.start( $tools.uploader );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Nav>
|
||||
{#if !!$running}
|
||||
<ToolRunningStatus/>
|
||||
{:else}
|
||||
<OffloadStatus bind:flyoutButton bind:expanded bind:hasFocus>
|
||||
<svelte:fragment slot="flyout">
|
||||
<OffloadStatusFlyout bind:expanded bind:hasFocus bind:buttonRef={flyoutButton}>
|
||||
<svelte:fragment slot="footer">
|
||||
<PanelRow footer class="offload-remaining">
|
||||
<Button
|
||||
primary
|
||||
disabled={offloadRemainingDisabledMessage}
|
||||
title={offloadRemainingDisabledMessage}
|
||||
on:click={startOffload}
|
||||
>
|
||||
{$offloadRemainingWithCount}
|
||||
</Button>
|
||||
</PanelRow>
|
||||
|
||||
<PanelRow footer class="licence">
|
||||
<div class="details">
|
||||
<p class="title">{$strings.plan_usage_title}</p>
|
||||
<p>{$licence.plan_usage}</p>
|
||||
</div>
|
||||
{#if !$licence.is_set}
|
||||
<a href="/license" use:link>
|
||||
{$strings.activate_licence}
|
||||
</a>
|
||||
{:else if $licence.limit_info.limit !== 0}
|
||||
<a href={$urls.licenses} target="_blank" class="upgrade">
|
||||
{$strings.upgrade_plan_cta}
|
||||
</a>
|
||||
{/if}
|
||||
</PanelRow>
|
||||
</svelte:fragment>
|
||||
</OffloadStatusFlyout>
|
||||
</svelte:fragment>
|
||||
</OffloadStatus>
|
||||
{/if}
|
||||
</Nav>
|
||||
33
ui/pro/NoTools.svelte
Normal file
33
ui/pro/NoTools.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import {strings, urls} from "../js/stores";
|
||||
import Upsell from "../components/Upsell.svelte";
|
||||
|
||||
let benefits = [
|
||||
{
|
||||
icon: $urls.assets + "img/icon/offload-remaining.svg",
|
||||
alt: "offload icon",
|
||||
text: $strings.tools_uppsell_benefits.offload,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + "img/icon/download.svg",
|
||||
alt: "download icon",
|
||||
text: $strings.tools_uppsell_benefits.download,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + "img/icon/remove-from-bucket.svg",
|
||||
alt: "remove from bucket icon",
|
||||
text: $strings.tools_uppsell_benefits.remove_bucket,
|
||||
},
|
||||
{
|
||||
icon: $urls.assets + "img/icon/remove-from-server.svg",
|
||||
alt: "remove from server icon",
|
||||
text: $strings.tools_uppsell_benefits.remove_server,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Upsell {benefits}>
|
||||
<div slot="heading">{$strings.no_tools_header}</div>
|
||||
|
||||
<div slot="description">{@html $strings.no_tools_description}</div>
|
||||
</Upsell>
|
||||
62
ui/pro/RemoveLocalFilesPromptPage.svelte
Normal file
62
ui/pro/RemoveLocalFilesPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "remove-local-files";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.remove_local_files;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
201
ui/pro/Settings.svelte
Normal file
201
ui/pro/Settings.svelte
Normal file
@@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import {onMount} from "svelte";
|
||||
import {
|
||||
strings,
|
||||
config,
|
||||
defaultStorageProvider,
|
||||
settingsLocked,
|
||||
notifications,
|
||||
current_settings,
|
||||
needs_access_keys,
|
||||
needs_refresh,
|
||||
counts,
|
||||
settings_notifications,
|
||||
settings,
|
||||
settings_changed,
|
||||
preStateUpdateCallbacks,
|
||||
postStateUpdateCallbacks
|
||||
} from "../js/stores";
|
||||
import {
|
||||
licence,
|
||||
running,
|
||||
tools,
|
||||
toolsLocked,
|
||||
assetsNeedsRefresh,
|
||||
assetsSettingsLocked,
|
||||
assetsSettings,
|
||||
assetsSettingsChanged
|
||||
} from "./stores";
|
||||
import {pages} from "../js/routes";
|
||||
import {defaultPages} from "../js/defaultPages";
|
||||
import {addPages} from "./pages";
|
||||
import {settingsNotifications} from "../js/settingsNotifications";
|
||||
import {toolSettingsNotifications} from "./toolSettingsNotifications";
|
||||
import Settings from "../components/Settings.svelte";
|
||||
import Header from "./Header.svelte";
|
||||
import Nav from "./Nav.svelte";
|
||||
import Pages from "../components/Pages.svelte";
|
||||
|
||||
export let init = {};
|
||||
|
||||
// During initialization set config store to passed in values to avoid undefined values in components during mount.
|
||||
// This saves having to do a lot of checking of values before use.
|
||||
config.set( init );
|
||||
pages.set( defaultPages );
|
||||
|
||||
// We need a disassociated copy of the initial tools info to start with.
|
||||
tools.updateTools( { tools: { ...$config.tools } } );
|
||||
|
||||
// We need a disassociated copy of the initial assets settings to work with.
|
||||
assetsSettings.set( { ...$config.assets_settings } );
|
||||
|
||||
// Add Pro specific pages.
|
||||
addPages( $tools );
|
||||
|
||||
/**
|
||||
* Handles state update event's changes to config.
|
||||
*
|
||||
* @param {Object} config
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleStateUpdate( config ) {
|
||||
let _settingsLocked = false;
|
||||
let _toolsLocked = false;
|
||||
let _assetsSettingsLocked = false;
|
||||
|
||||
// All settings need to be locked?
|
||||
if ( config.upgrades.is_upgrading ) {
|
||||
_settingsLocked = true;
|
||||
_toolsLocked = true;
|
||||
_assetsSettingsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-all-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
heading: config.upgrades.locked_notifications[ config.upgrades.running_upgrade ],
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
|
||||
if ( $settings_changed ) {
|
||||
settings.reset();
|
||||
}
|
||||
|
||||
if ( $assetsSettingsChanged ) {
|
||||
assetsSettings.reset();
|
||||
}
|
||||
} else {
|
||||
notifications.delete( "as3cf-all-settings-locked" );
|
||||
}
|
||||
|
||||
// Media settings need to be locked?
|
||||
if ( $needs_refresh ) {
|
||||
_settingsLocked = true;
|
||||
_toolsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-media-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
heading: $strings.needs_refresh,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else if ( $running ) {
|
||||
_settingsLocked = true;
|
||||
|
||||
const tool = $tools[ $running ];
|
||||
const notification = {
|
||||
id: "as3cf-media-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
heading: tool.locked_notification,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
|
||||
if ( $settings_changed ) {
|
||||
settings.reset();
|
||||
}
|
||||
} else {
|
||||
notifications.delete( "as3cf-media-settings-locked" );
|
||||
}
|
||||
|
||||
// Assets settings need to be locked?
|
||||
if ( $assetsNeedsRefresh ) {
|
||||
_assetsSettingsLocked = true;
|
||||
|
||||
const notification = {
|
||||
id: "as3cf-assets-settings-locked",
|
||||
type: "warning",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "assets",
|
||||
heading: $strings.needs_refresh,
|
||||
icon: "notification-locked.svg",
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else {
|
||||
notifications.delete( "as3cf-assets-settings-locked" );
|
||||
}
|
||||
|
||||
$settingsLocked = _settingsLocked;
|
||||
$toolsLocked = _toolsLocked;
|
||||
$assetsSettingsLocked = _assetsSettingsLocked;
|
||||
|
||||
// Show a persistent error notice if bucket can't be accessed.
|
||||
if ( $needs_access_keys && ($settings.provider !== $defaultStorageProvider || $settings.bucket.length !== 0) ) {
|
||||
const notification = {
|
||||
id: "as3cf-needs-access-keys",
|
||||
type: "error",
|
||||
dismissible: false,
|
||||
only_show_on_tab: "media",
|
||||
hide_on_parent: true,
|
||||
heading: $strings.needs_access_keys,
|
||||
plainHeading: true
|
||||
};
|
||||
notifications.add( notification );
|
||||
} else {
|
||||
notifications.delete( "as3cf-needs-access-keys" );
|
||||
}
|
||||
}
|
||||
|
||||
// Catch changes to running tool as soon as possible.
|
||||
$: if ( $running ) {
|
||||
handleStateUpdate( $config );
|
||||
}
|
||||
|
||||
// Catch changes to needing access credentials as soon as possible.
|
||||
$: if ( $needs_access_keys ) {
|
||||
handleStateUpdate( $config );
|
||||
}
|
||||
|
||||
onMount( () => {
|
||||
// Make sure state dependent data is up-to-date.
|
||||
handleStateUpdate( $config );
|
||||
|
||||
// When state info is fetched we need some extra processing of the data.
|
||||
preStateUpdateCallbacks.update( _callables => {
|
||||
return [..._callables, assetsSettings.updateSettings];
|
||||
} );
|
||||
|
||||
postStateUpdateCallbacks.update( _callables => {
|
||||
return [..._callables, tools.updateTools, handleStateUpdate];
|
||||
} );
|
||||
} );
|
||||
|
||||
// Make sure all inline notifications are in place.
|
||||
$: settings_notifications.update( ( notices ) => settingsNotifications.process( notices, $settings, $current_settings, $strings ) );
|
||||
$: settings_notifications.update( ( notices ) => toolSettingsNotifications.process( notices, $settings, $current_settings, $strings, $counts, $licence ) );
|
||||
</script>
|
||||
|
||||
<Settings header={Header}>
|
||||
<Pages nav={Nav}/>
|
||||
</Settings>
|
||||
139
ui/pro/SupportForm.svelte
Normal file
139
ui/pro/SupportForm.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script>
|
||||
import {diagnostics, notifications, strings} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
let email = "";
|
||||
let subject = "";
|
||||
let message = "";
|
||||
let includeDiagnostics = true;
|
||||
|
||||
/**
|
||||
* Potentially returns a reason that the Submit button is disabled.
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {string} subject
|
||||
* @param {string} message
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getDisabledReason( email, subject, message ) {
|
||||
let reason = "";
|
||||
|
||||
if ( !email || !subject || !message ) {
|
||||
reason = "Email, Subject and Message required.";
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
|
||||
$: disabledReason = getDisabledReason( email, subject, message );
|
||||
|
||||
/**
|
||||
* Handles a Submit button click.
|
||||
*
|
||||
* @param {Object} event
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function submitSupportRequest( event ) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append( "email", email );
|
||||
formData.append( "subject", subject );
|
||||
formData.append( "message", message );
|
||||
|
||||
if ( includeDiagnostics ) {
|
||||
formData.append( "local-diagnostic", "1" );
|
||||
formData.append( "local-diagnostic-content", $diagnostics );
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(
|
||||
$licence.support_url,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
const notice = $strings.send_email_post_error + error.message;
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: notice
|
||||
} );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
if ( json.hasOwnProperty( "errors" ) ) {
|
||||
for ( const [key, value] of Object.entries( json.errors ) ) {
|
||||
const notice = $strings.send_email_api_error + value;
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: notice
|
||||
} );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ( json.hasOwnProperty( "success" ) && json.success === 1 ) {
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "success",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: $strings.send_email_success
|
||||
} );
|
||||
|
||||
email = "";
|
||||
subject = "";
|
||||
message = "";
|
||||
includeDiagnostics = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.add( {
|
||||
id: "support-send-email-response",
|
||||
type: "error",
|
||||
dismissible: true,
|
||||
only_show_on_tab: "support",
|
||||
message: $strings.send_email_unexpected_error
|
||||
} );
|
||||
}
|
||||
</script>
|
||||
|
||||
<label for="email" class="input-label">From</label>
|
||||
<select name="email" id="email" bind:value={email}>
|
||||
{#each $licence.support_email_addresses as supportEmail}
|
||||
<option value={supportEmail}>{supportEmail}</option>
|
||||
{/each}
|
||||
<option value="">{$strings.select_email}</option>
|
||||
</select>
|
||||
<p class="note">{@html $strings.email_note}</p>
|
||||
<input type="text" id="subject" name="subject" bind:value={subject} minlength="4" placeholder={$strings.email_subject_placeholder}>
|
||||
<textarea id="message" name="message" bind:value={message} rows="8" placeholder={$strings.email_message_placeholder}></textarea>
|
||||
<div class="actions">
|
||||
<div class="checkbox">
|
||||
<label for="include-diagnostics">
|
||||
<input type="checkbox" id="include-diagnostics" name="include-diagnostics" bind:checked={includeDiagnostics}>{$strings.attach_diagnostics}
|
||||
</label>
|
||||
</div>
|
||||
<Button primary on:click={submitSupportRequest} disabled={disabledReason} title={disabledReason}>{$strings.send_email}</Button>
|
||||
</div>
|
||||
<p class="note first">{$strings.having_trouble}</p>
|
||||
<p class="note">{@html $strings.email_instead}</p>
|
||||
76
ui/pro/SupportPage.svelte
Normal file
76
ui/pro/SupportPage.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {link} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {licence} from "./stores";
|
||||
import SupportPage from "../components/SupportPage.svelte";
|
||||
import DocumentationSidebar from "./DocumentationSidebar.svelte";
|
||||
import SupportForm from "./SupportForm.svelte";
|
||||
import Notification from "../components/Notification.svelte";
|
||||
|
||||
export let name = "support";
|
||||
|
||||
/**
|
||||
* Potentially returns an error message detailing a problem with the currently set license key.
|
||||
*
|
||||
* @param {Object} licence
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getLicenceError( licence ) {
|
||||
// If there are any errors, just return the first (there's usually only 1 anyway).
|
||||
if ( licence.hasOwnProperty( "errors" ) && Object.values( licence.errors ).length > 0 ) {
|
||||
return Object.values( licence.errors )[ 0 ];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
$: licenceError = getLicenceError( $licence );
|
||||
</script>
|
||||
|
||||
{#if $licence.is_set}
|
||||
{#if $licence.is_valid && licenceError.length === 0}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<p class="licence-type" slot="header">{@html $licence.your_active_licence}</p>
|
||||
<svelte:fragment slot="content">
|
||||
<SupportForm/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{:else}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<svelte:fragment slot="content">
|
||||
<Notification warning inline>
|
||||
<p>
|
||||
{@html licenceError}
|
||||
</p>
|
||||
</Notification>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{/if}
|
||||
{:else}
|
||||
<SupportPage {name} title={$strings.email_support_title} on:routeEvent>
|
||||
<svelte:fragment slot="content">
|
||||
<Notification warning inline>
|
||||
<p>
|
||||
{$strings.licence_not_entered}
|
||||
<a href="/license" use:link>
|
||||
{$strings.please_enter_licence}
|
||||
</a>
|
||||
</p>
|
||||
<p>{$strings.once_licence_entered}</p>
|
||||
</Notification>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<DocumentationSidebar/>
|
||||
</svelte:fragment>
|
||||
</SupportPage>
|
||||
{/if}
|
||||
76
ui/pro/ToolNotification.svelte
Normal file
76
ui/pro/ToolNotification.svelte
Normal file
@@ -0,0 +1,76 @@
|
||||
<script>
|
||||
import {fade, slide} from "svelte/transition";
|
||||
import {api, strings} from "../js/stores";
|
||||
import Notification from "../components/Notification.svelte";
|
||||
|
||||
export let notification;
|
||||
|
||||
let expanded = true;
|
||||
|
||||
/**
|
||||
* Handles Dismiss All Errors for item click.
|
||||
*
|
||||
* @param {string} tool_key
|
||||
* @param {Object} item
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function dismissAll( tool_key, item ) {
|
||||
await api.delete( "tools", {
|
||||
id: tool_key,
|
||||
blog_id: item.blog_id,
|
||||
source_type: item.source_type,
|
||||
source_id: item.source_id
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Dismiss Individual Error for item click.
|
||||
*
|
||||
* @param {string} tool_key
|
||||
* @param {Object} item
|
||||
* @param {number} index
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function dismissError( tool_key, item, index ) {
|
||||
await api.delete( "tools", {
|
||||
id: tool_key,
|
||||
blog_id: item.blog_id,
|
||||
source_type: item.source_type,
|
||||
source_id: item.source_id,
|
||||
errors: index
|
||||
} );
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if notification.hasOwnProperty( "class" ) && notification.class === "tool-error" && notification.hasOwnProperty( "errors" )}
|
||||
<Notification notification={notification} expandable bind:expanded>
|
||||
<svelte:fragment slot="details">
|
||||
{#if expanded}
|
||||
<div class="details" transition:slide>
|
||||
{#each notification.errors.details as item, index}
|
||||
<div class="item" transition:fade>
|
||||
<div class="summary">
|
||||
<div class="title">
|
||||
{(index + 1) + ". " + item.source_type_name}
|
||||
<a href={item.edit_url.url}>#{item.source_id}</a>
|
||||
</div>
|
||||
<button class="dismiss" on:click|preventDefault={() => dismissAll(notification.errors.tool_key, item)}>{$strings.dismiss}</button>
|
||||
</div>
|
||||
<ul class="detail">
|
||||
{#each item.messages as message, index}
|
||||
<li>{@html message}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Notification>
|
||||
{:else}
|
||||
<Notification notification={notification}>
|
||||
<slot/>
|
||||
</Notification>
|
||||
{/if}
|
||||
178
ui/pro/ToolPanel.svelte
Normal file
178
ui/pro/ToolPanel.svelte
Normal file
@@ -0,0 +1,178 @@
|
||||
<script>
|
||||
import {bucket_writable, strings, urls} from "../js/stores";
|
||||
import {running, tools, toolsLocked} from "./stores";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import Button from "../components/Button.svelte";
|
||||
import ProgressBar from "../components/ProgressBar.svelte";
|
||||
import ToolRunningButtons from "./ToolRunningButtons.svelte";
|
||||
import {numToString} from "../js/numToString";
|
||||
|
||||
export let tool = {};
|
||||
|
||||
// Total processed related variables.
|
||||
$: showTotal = !!tool.hasOwnProperty( "total_progress" );
|
||||
$: initial = !!(showTotal && tool.total_progress < 1);
|
||||
$: partialComplete = !!(showTotal && tool.total_progress > 0 && tool.total_progress < 100);
|
||||
$: complete = !!(showTotal && !initial && !partialComplete);
|
||||
|
||||
// In progress related variables.
|
||||
$: isRunning = !!($running && $running === tool.id);
|
||||
$: starting = !!(isRunning && tool.progress < 1 && !tool.is_paused);
|
||||
// Buttons should be disabled if another tool is running, current tool is in process of pausing or cancelling, or all tools locked.
|
||||
$: disabled = ($running && $running !== tool.id) || (tool.is_processing && tool.is_paused) || tool.is_cancelled || $toolsLocked;
|
||||
$: disabled_bucket_access = tool.requires_bucket_access && !$bucket_writable;
|
||||
|
||||
/**
|
||||
* Returns the numeric percentage progress for the running job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
* @param {boolean} showTotal
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function getPercentComplete( tool, isRunning, showTotal ) {
|
||||
if ( isRunning ) {
|
||||
return tool.progress;
|
||||
} else if ( showTotal ) {
|
||||
return tool.total_progress;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: percentComplete = getPercentComplete( tool, isRunning, showTotal );
|
||||
|
||||
/**
|
||||
* Returns state dependent icon for tool.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
* @return {string}
|
||||
*/
|
||||
function getIcon( tool, isRunning ) {
|
||||
const icon = tools.icon( tool, isRunning, false );
|
||||
|
||||
return $urls.assets + "img/icon/" + icon;
|
||||
}
|
||||
|
||||
$: icon = getIcon( tool, isRunning );
|
||||
|
||||
/**
|
||||
* Potentially returns a map of tools that are related to the current tool.
|
||||
*
|
||||
* Map is keyed by tool's id (key), values are tool objects.
|
||||
*
|
||||
* @param {Object} tool
|
||||
*
|
||||
* @return {Map<string, object>}
|
||||
*/
|
||||
function getRelatedTools( tool ) {
|
||||
let related = new Map();
|
||||
|
||||
if ( tool.hasOwnProperty( "related_tools" ) && tool.related_tools.length > 0 ) {
|
||||
tool.related_tools.forEach( ( key ) => {
|
||||
if ( $tools.hasOwnProperty( key ) ) {
|
||||
related.set( key, $tools[ key ] );
|
||||
}
|
||||
} )
|
||||
}
|
||||
|
||||
return related;
|
||||
}
|
||||
|
||||
$: relatedTools = getRelatedTools( tool );
|
||||
|
||||
/**
|
||||
* Starts a tool's job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
*/
|
||||
function handleStartTool( tool ) {
|
||||
tools.start( tool );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Start button click.
|
||||
*/
|
||||
function handleStart() {
|
||||
handleStartTool( tool );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel multi class="tools-panel">
|
||||
<PanelRow header>
|
||||
<img src={icon} type="image/svg+xml" alt={tool.title}>
|
||||
{#if showTotal}
|
||||
{#if initial}
|
||||
<h3>{@html tool.title}</h3>
|
||||
{:else if partialComplete}
|
||||
<h3>{@html tool.title_partial_complete}</h3>
|
||||
{:else}
|
||||
<h3>{@html tool.title_complete}</h3>
|
||||
{/if}
|
||||
{:else}
|
||||
<h3>{@html tool.title}</h3>
|
||||
{/if}
|
||||
<div class="buttons-right">
|
||||
{#if isRunning}
|
||||
<ToolRunningButtons {tool} {disabled}/>
|
||||
{:else}
|
||||
{#if complete}
|
||||
<!-- 🎉 -->
|
||||
{:else if disabled_bucket_access}
|
||||
<Button primary disabled={true} title={$strings.disabled_tool_bucket_access}>{@html partialComplete ? tool.button_partial_complete : tool.button}</Button>
|
||||
{:else if initial}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button}</Button>
|
||||
{:else if partialComplete}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button_partial_complete}</Button>
|
||||
{:else}
|
||||
<Button primary {disabled} title={disabled ? $strings.disabled_tool_button : ""} on:click={handleStart}>{@html tool.button}</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</PanelRow>
|
||||
{#if complete || partialComplete || isRunning}
|
||||
<PanelRow class="body flex-row show-progress">
|
||||
<div class="status">
|
||||
{#if isRunning}
|
||||
<h4>
|
||||
<strong>{getPercentComplete( tool, isRunning, showTotal )}%</strong> ({numToString( tool.queue.processed )}/{numToString( tool.queue.total )})
|
||||
{@html tool.status_description ? " " + tool.status_description : " " + tool.busy_description}
|
||||
</h4>
|
||||
{:else }
|
||||
<h4>{@html tool.progress_description}</h4>
|
||||
{/if}
|
||||
<slot name="status-right" {isRunning}/>
|
||||
</div>
|
||||
<ProgressBar
|
||||
{percentComplete}
|
||||
{starting}
|
||||
running={isRunning}
|
||||
paused={tool.is_paused}
|
||||
title={! isRunning && showTotal ? "(" + numToString(tool.total_processed) + "/" + numToString(tool.total_items) + ")" : ""}
|
||||
/>
|
||||
</PanelRow>
|
||||
{/if}
|
||||
{#if !complete && !partialComplete && !isRunning}
|
||||
<PanelRow class="body flex-row">
|
||||
<p class="desc">{@html tool.more_info}</p>
|
||||
</PanelRow>
|
||||
{#if !disabled && relatedTools.size > 0 }
|
||||
<PanelRow class="body flex-column" footer>
|
||||
{#each [...relatedTools] as [key, relatedTool] }
|
||||
<p>
|
||||
<a
|
||||
href={$urls.settings}
|
||||
on:click|preventDefault={() => handleStartTool(relatedTool)}
|
||||
title={relatedTool.more_info}
|
||||
>
|
||||
{relatedTool.title}
|
||||
</a>
|
||||
</p>
|
||||
{/each}
|
||||
</PanelRow>
|
||||
{/if}
|
||||
{/if}
|
||||
</Panel>
|
||||
34
ui/pro/ToolRunningButtons.svelte
Normal file
34
ui/pro/ToolRunningButtons.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import {tools} from "./stores";
|
||||
import {strings} from "../js/stores";
|
||||
import Button from "../components/Button.svelte";
|
||||
|
||||
export let tool = {};
|
||||
export let disabled = false;
|
||||
export let small = false;
|
||||
|
||||
/**
|
||||
* Handles a Pause or Resume button click.
|
||||
*/
|
||||
function handlePauseResume() {
|
||||
tools.pauseResume( tool );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Cancel button click.
|
||||
*/
|
||||
function handleCancel() {
|
||||
tools.cancel( tool );
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if tool}
|
||||
<Button outline {small} {disabled} class="pause" on:click={handlePauseResume}>
|
||||
{#if tool.is_paused}
|
||||
{$strings.resume_button}
|
||||
{:else}
|
||||
{$strings.pause_button}
|
||||
{/if}
|
||||
</Button>
|
||||
<Button outline {small} {disabled} on:click={handleCancel}>{$strings.cancel_button}</Button>
|
||||
{/if}
|
||||
94
ui/pro/ToolRunningStatus.svelte
Normal file
94
ui/pro/ToolRunningStatus.svelte
Normal file
@@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import {push} from "svelte-spa-router";
|
||||
import {urls} from "../js/stores";
|
||||
import {running, tools, toolsLocked} from "./stores";
|
||||
import {numToShortString, numToString} from "../js/numToString";
|
||||
import ProgressBar from "../components/ProgressBar.svelte";
|
||||
import ToolRunningButtons from "./ToolRunningButtons.svelte";
|
||||
|
||||
/**
|
||||
* Returns the currently running tool's details.
|
||||
*
|
||||
* @param {Object} tools
|
||||
* @param {string} running
|
||||
*
|
||||
* @return {unknown}
|
||||
*/
|
||||
function runningTool( tools, running ) {
|
||||
return Object.values( tools ).find( ( tool ) => tool.id === running );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status description for tool.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function toolStatus( tool, isRunning ) {
|
||||
if ( !isRunning ) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if ( tool.short_status_description ) {
|
||||
return tool.short_status_description;
|
||||
}
|
||||
|
||||
return tool.busy_description;
|
||||
}
|
||||
|
||||
$: isRunning = !!$running;
|
||||
$: tool = runningTool( $tools, $running );
|
||||
$: icon = tools.icon( tool, isRunning, true );
|
||||
|
||||
// Buttons should be disabled if another tool is running, current tool is in process of pausing or cancelling, or all tools locked.
|
||||
$: disabled = isRunning && (($running && $running !== tool.id) || (tool.is_processing && tool.is_paused) || tool.is_cancelled || $toolsLocked);
|
||||
|
||||
$: starting = !!(isRunning && tool.progress < 1 && !tool.is_paused);
|
||||
$: status = isRunning ? "(" + numToShortString( tool.queue.processed ) + "/" + numToShortString( tool.queue.total ) + ") " + toolStatus( tool, isRunning ) : "";
|
||||
$: title = isRunning ? tool.name + ": " + tool.progress + "% (" + numToString( tool.queue.processed ) + "/" + numToString( tool.queue.total ) + ")" : "";
|
||||
|
||||
/**
|
||||
* Returns the numeric percentage progress for the running job.
|
||||
*
|
||||
* @param {Object} tool
|
||||
* @param {boolean} isRunning
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
function getPercentComplete( tool, isRunning ) {
|
||||
if ( isRunning ) {
|
||||
return tool.progress;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$: percentComplete = getPercentComplete( tool, isRunning );
|
||||
</script>
|
||||
|
||||
{#if tool}
|
||||
<div class="nav-status-wrapper tool-running">
|
||||
<!-- TODO: Fix a11y. -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="nav-status" {title} on:click={() => push("/tools")}>
|
||||
<p class="status-text" {title}>
|
||||
<strong>{tool.progress}%</strong>
|
||||
<span> {@html status}</span>
|
||||
</p>
|
||||
<ProgressBar
|
||||
{percentComplete}
|
||||
{starting}
|
||||
running={isRunning}
|
||||
paused={tool.is_paused}
|
||||
{title}
|
||||
/>
|
||||
<div class="animation-running" {title}>
|
||||
<img src="{$urls.assets + 'img/icon/' + icon}" alt="{tool.status_description}"/>
|
||||
</div>
|
||||
</div>
|
||||
<ToolRunningButtons {tool} {disabled} small/>
|
||||
</div>
|
||||
{/if}
|
||||
29
ui/pro/ToolsPage.svelte
Normal file
29
ui/pro/ToolsPage.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import {setContext} from "svelte";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools, toolsLocked} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import ToolPanel from "./ToolPanel.svelte";
|
||||
import NoTools from "./NoTools.svelte";
|
||||
|
||||
export let name = "tools";
|
||||
|
||||
// Let all child components know if tools are currently locked.
|
||||
// All panels etc respond to settingsLocked, so we fake it here as we're not in a settings context.
|
||||
setContext( "settingsLocked", toolsLocked );
|
||||
</script>
|
||||
|
||||
<Page {name} on:routeEvent>
|
||||
<Notifications tab={name} component={ToolNotification}/>
|
||||
<h2 class="page-title">{$strings.tools_title}</h2>
|
||||
|
||||
<div class="tools-page wrapper">
|
||||
{#each Object.values( $tools ).filter( ( tool ) => tool.render ) as tool (tool.id)}
|
||||
<ToolPanel {tool}/>
|
||||
{:else}
|
||||
<NoTools/>
|
||||
{/each}
|
||||
</div>
|
||||
</Page>
|
||||
62
ui/pro/UpdateObjectACLsPromptSubPage.svelte
Normal file
62
ui/pro/UpdateObjectACLsPromptSubPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, getContext, hasContext} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
import {pop} from "svelte-spa-router";
|
||||
import {strings} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import SubPage from "../components/SubPage.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
|
||||
const tool = $tools.update_acls;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Parent page may want to be locked.
|
||||
let settingsLocked = writable( false );
|
||||
|
||||
if ( hasContext( "settingsLocked" ) ) {
|
||||
settingsLocked = getContext( "settingsLocked" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<SubPage name="update-acls" route="/storage/update-acls">
|
||||
<Panel
|
||||
heading={tool.title}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
skipText={$strings.no}
|
||||
nextText={$strings.yes}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</SubPage>
|
||||
62
ui/pro/UploaderPromptPage.svelte
Normal file
62
ui/pro/UploaderPromptPage.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import {createEventDispatcher, setContext} from "svelte";
|
||||
import {settingsLocked} from "../js/stores";
|
||||
import {tools} from "./stores";
|
||||
import Page from "../components/Page.svelte";
|
||||
import Notifications from "../components/Notifications.svelte";
|
||||
import ToolNotification from "./ToolNotification.svelte";
|
||||
import BackNextButtonsRow from "../components/BackNextButtonsRow.svelte";
|
||||
import Panel from "../components/Panel.svelte";
|
||||
import PanelRow from "../components/PanelRow.svelte";
|
||||
|
||||
export let name = "uploader";
|
||||
|
||||
// Let all child components know if settings are currently locked.
|
||||
setContext( "settingsLocked", settingsLocked );
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const tool = $tools.uploader;
|
||||
|
||||
/**
|
||||
* Handles a Skip button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleSkip() {
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Next button click.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async function handleNext() {
|
||||
await tools.start( tool );
|
||||
dispatch( "routeEvent", { event: "next", default: "/" } );
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page {name} subpage on:routeEvent>
|
||||
<Notifications tab="media" component={ToolNotification}/>
|
||||
|
||||
<Panel
|
||||
heading={tool.name}
|
||||
helpURL={tool.doc_url}
|
||||
helpDesc={tool.doc_desc}
|
||||
multi
|
||||
>
|
||||
<PanelRow class="body flex-column">
|
||||
<p>{@html tool.prompt}</p>
|
||||
</PanelRow>
|
||||
</Panel>
|
||||
|
||||
<BackNextButtonsRow
|
||||
on:skip={handleSkip}
|
||||
on:next={handleNext}
|
||||
nextText={tool.button}
|
||||
skipVisible={true}
|
||||
nextDisabled={$settingsLocked}
|
||||
/>
|
||||
</Page>
|
||||
615
ui/pro/pages.js
Normal file
615
ui/pro/pages.js
Normal file
@@ -0,0 +1,615 @@
|
||||
import {get} from "svelte/store";
|
||||
import {location} from "svelte-spa-router";
|
||||
import {
|
||||
bapa,
|
||||
ooe,
|
||||
counts,
|
||||
current_settings,
|
||||
storage_provider,
|
||||
strings
|
||||
} from "../js/stores";
|
||||
import {pages} from "../js/routes";
|
||||
import {licence} from "./stores";
|
||||
import AssetsPage from "./AssetsPage.svelte";
|
||||
import ToolsPage from "./ToolsPage.svelte";
|
||||
import LicencePage from "./LicencePage.svelte";
|
||||
import SupportPage from "./SupportPage.svelte";
|
||||
import UpdateObjectACLsPromptSubPage
|
||||
from "./UpdateObjectACLsPromptSubPage.svelte";
|
||||
import CopyBucketsPromptSubPage from "./CopyBucketsPromptSubPage.svelte";
|
||||
import MoveObjectsPromptPage from "./MoveObjectsPromptPage.svelte";
|
||||
import MovePublicObjectsPromptPage from "./MovePublicObjectsPromptPage.svelte";
|
||||
import MovePrivateObjectsPromptPage
|
||||
from "./MovePrivateObjectsPromptPage.svelte";
|
||||
import RemoveLocalFilesPromptPage from "./RemoveLocalFilesPromptPage.svelte";
|
||||
import UploaderPromptPage from "./UploaderPromptPage.svelte";
|
||||
import DownloaderPromptPage from "./DownloaderPromptPage.svelte";
|
||||
|
||||
export function addPages( enabledTools ) {
|
||||
pages.add(
|
||||
{
|
||||
position: 10,
|
||||
name: "assets",
|
||||
title: () => get( strings ).assets_tab_title,
|
||||
nav: true,
|
||||
route: "/assets",
|
||||
component: AssetsPage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 20,
|
||||
name: "tools",
|
||||
title: () => get( strings ).tools_tab_title,
|
||||
nav: true,
|
||||
route: "/tools",
|
||||
component: ToolsPage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 90,
|
||||
name: "licence",
|
||||
title: () => get( strings ).licence_tab_title,
|
||||
nav: true,
|
||||
route: "/license",
|
||||
component: LicencePage
|
||||
}
|
||||
);
|
||||
pages.add(
|
||||
{
|
||||
position: 100,
|
||||
name: "support",
|
||||
title: () => get( strings ).support_tab_title,
|
||||
nav: true,
|
||||
route: "/support",
|
||||
component: SupportPage
|
||||
}
|
||||
);
|
||||
|
||||
// Update ACLs tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "update_acls" ) ) {
|
||||
const updateACLs = {
|
||||
position: 240,
|
||||
name: "update-acls",
|
||||
title: () => enabledTools.update_acls.name,
|
||||
subNav: true,
|
||||
route: "/storage/update-acls",
|
||||
component: UpdateObjectACLsPromptSubPage,
|
||||
enabled: () => {
|
||||
// Nothing to update?
|
||||
if (
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1 ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Current Storage Provider never allows ACLs to be disabled.
|
||||
if ( get( storage_provider ).requires_acls ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If either Block All Public Access or Object Ownership turned on,
|
||||
// we should not update ACLs.
|
||||
if ( get( bapa ) || get( ooe ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update ACLs if BAPA just turned off.
|
||||
if (
|
||||
get( storage_provider ).block_public_access_supported &&
|
||||
get( current_settings ).hasOwnProperty( "block-public-access" ) &&
|
||||
updateACLs.blockPublicAccess !== get( current_settings )[ "block-public-access" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update ACLs if OOE just turned off.
|
||||
if (
|
||||
get( storage_provider ).object_ownership_supported &&
|
||||
get( current_settings ).hasOwnProperty( "object-ownership-enforced" ) &&
|
||||
updateACLs.objectOwnershipEnforced !== get( current_settings )[ "object-ownership-enforced" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!updateACLs.enabled()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If currently in /storage/security route, then update-acls is next.
|
||||
return get( location ) === "/storage/security";
|
||||
},
|
||||
blockPublicAccess: get( current_settings ).hasOwnProperty( "bucket" ) && get( current_settings ).hasOwnProperty( "block-public-access" ) ? get( current_settings )[ "block-public-access" ] : false,
|
||||
objectOwnershipEnforced: get( current_settings ).hasOwnProperty( "bucket" ) && get( current_settings ).hasOwnProperty( "object-ownership-enforced" ) ? get( current_settings )[ "object-ownership-enforced" ] : false,
|
||||
setInitialProperties: ( data ) => {
|
||||
if ( data.hasOwnProperty( "settings" ) && data.settings.hasOwnProperty( "bucket" ) ) {
|
||||
if ( data.settings.hasOwnProperty( "block-public-access" ) ) {
|
||||
updateACLs.blockPublicAccess = data.settings[ "block-public-access" ];
|
||||
} else {
|
||||
updateACLs.blockPublicAccess = false;
|
||||
}
|
||||
|
||||
if ( data.settings.hasOwnProperty( "object-ownership-enforced" ) ) {
|
||||
updateACLs.objectOwnershipEnforced = data.settings[ "object-ownership-enforced" ];
|
||||
} else {
|
||||
updateACLs.objectOwnershipEnforced = false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => updateACLs.isNextRoute( data ),
|
||||
"bucket-security": ( data ) => updateACLs.isNextRoute( data ),
|
||||
"settings.save": ( data ) => updateACLs.setInitialProperties( data ),
|
||||
"page.initial.settings": ( data ) => updateACLs.setInitialProperties( data )
|
||||
}
|
||||
};
|
||||
pages.add( updateACLs );
|
||||
}
|
||||
|
||||
// Copy Files tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "copy_buckets" ) ) {
|
||||
const copyBuckets = {
|
||||
position: 250,
|
||||
name: "copy-buckets",
|
||||
title: () => enabledTools.copy_buckets.name,
|
||||
subNav: true,
|
||||
route: "/storage/copy-buckets",
|
||||
component: CopyBucketsPromptSubPage,
|
||||
enabled: () => {
|
||||
return get( counts ).offloaded > 0 && get( current_settings ).hasOwnProperty( "bucket" ) && copyBuckets.bucket !== get( current_settings ).bucket;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!copyBuckets.enabled()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If currently in any of the below routes, then copy-buckets is next if not gazumped.
|
||||
return get( location ) === "/storage/bucket" || get( location ) === "/storage/security" || get( location ) === "/storage/update-acls";
|
||||
},
|
||||
bucket: get( current_settings ).hasOwnProperty( "bucket" ) ? get( current_settings ).bucket : "",
|
||||
setInitialBucket: ( data ) => {
|
||||
if ( data.hasOwnProperty( "settings" ) && data.settings.hasOwnProperty( "bucket" ) ) {
|
||||
copyBuckets.bucket = data.settings.bucket;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"settings.save": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"bucket-security": ( data ) => copyBuckets.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => copyBuckets.setInitialBucket( data )
|
||||
}
|
||||
};
|
||||
pages.add( copyBuckets );
|
||||
}
|
||||
|
||||
// Move Public/Private Objects tool prompt.
|
||||
if (
|
||||
enabledTools.hasOwnProperty( "move_objects" ) &&
|
||||
enabledTools.hasOwnProperty( "move_public_objects" ) &&
|
||||
enabledTools.hasOwnProperty( "move_private_objects" )
|
||||
) {
|
||||
const moveObjects = {
|
||||
position: 400,
|
||||
name: "move-objects",
|
||||
title: () => enabledTools.move_objects.name,
|
||||
route: "/prompt/move-objects",
|
||||
component: MoveObjectsPromptPage,
|
||||
publicPathChanged: ( data ) => {
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Year/Month disabled - never show prompt.
|
||||
if (
|
||||
data.changed_settings.includes( "use-yearmonth-folders" ) &&
|
||||
get( current_settings ).hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
!get( current_settings )[ "use-yearmonth-folders" ]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Object Versioning disabled - never show prompt.
|
||||
if (
|
||||
data.changed_settings.includes( "object-versioning" ) &&
|
||||
get( current_settings ).hasOwnProperty( "object-versioning" ) &&
|
||||
!get( current_settings )[ "object-versioning" ]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Path enabled/disabled.
|
||||
if (
|
||||
data.changed_settings.includes( "enable-object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-object-prefix" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Path changed while enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-object-prefix" ) &&
|
||||
get( current_settings )[ "enable-object-prefix" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Year/Month enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "use-yearmonth-folders" ) &&
|
||||
get( current_settings ).hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
get( current_settings )[ "use-yearmonth-folders" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Object Versioning enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "object-versioning" ) &&
|
||||
get( current_settings ).hasOwnProperty( "object-versioning" ) &&
|
||||
get( current_settings )[ "object-versioning" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
privatePathChanged: ( data ) => {
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Signed URLs enabled/disabled.
|
||||
if (
|
||||
data.changed_settings.includes( "enable-signed-urls" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-signed-urls" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Signed URLs prefix changed while enabled.
|
||||
if (
|
||||
data.changed_settings.includes( "signed-urls-object-prefix" ) &&
|
||||
get( current_settings ).hasOwnProperty( "enable-signed-urls" ) &&
|
||||
get( current_settings )[ "enable-signed-urls" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.publicPathChanged( data ) && moveObjects.privatePathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => moveObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( moveObjects );
|
||||
|
||||
const movePublicObjects = {
|
||||
position: 410,
|
||||
name: "move-public-objects",
|
||||
title: () => enabledTools.move_public_objects.name,
|
||||
route: "/prompt/move-public-objects",
|
||||
component: MovePublicObjectsPromptPage,
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.publicPathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => movePublicObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( movePublicObjects );
|
||||
|
||||
const movePrivateObjects = {
|
||||
position: 420,
|
||||
name: "move-private-objects",
|
||||
title: () => enabledTools.move_private_objects.name,
|
||||
route: "/prompt/move-private-objects",
|
||||
component: MovePrivateObjectsPromptPage,
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moveObjects.privatePathChanged( data );
|
||||
},
|
||||
events: {
|
||||
"settings.save": ( data ) => movePrivateObjects.isNextRoute( data )
|
||||
}
|
||||
};
|
||||
pages.add( movePrivateObjects );
|
||||
}
|
||||
|
||||
// Remove Local Files tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "remove_local_files" ) ) {
|
||||
const removeLocalFiles = {
|
||||
position: 430,
|
||||
name: "remove-local-files",
|
||||
title: () => enabledTools.remove_local_files.name,
|
||||
route: "/prompt/remove-local-files",
|
||||
component: RemoveLocalFilesPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < removeLocalFiles.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
removeLocalFile: get( current_settings ).hasOwnProperty( "remove-local-file" ) ? get( current_settings )[ "remove-local-file" ] : false,
|
||||
setInitialRemoveLocalFile: ( data ) => {
|
||||
if (
|
||||
get( location ) !== removeLocalFiles.route &&
|
||||
!removeLocalFiles.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "remove-local-file" )
|
||||
) {
|
||||
removeLocalFiles.removeLocalFile = data.settings[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Remove Local Files turned on.
|
||||
if (
|
||||
data.changed_settings.includes( "remove-local-file" ) &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
removeLocalFiles.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
removeLocalFiles.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
removeLocalFiles.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "remove-local-file" ) ) {
|
||||
removeLocalFiles.removeLocalFile = get( current_settings )[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => removeLocalFiles.isNextRoute( data ),
|
||||
"settings.save": ( data ) => removeLocalFiles.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => removeLocalFiles.setInitialRemoveLocalFile( data )
|
||||
}
|
||||
};
|
||||
pages.add( removeLocalFiles );
|
||||
}
|
||||
|
||||
// Uploader tool prompt.
|
||||
if ( enabledTools.hasOwnProperty( "uploader" ) ) {
|
||||
const uploader = {
|
||||
position: 440,
|
||||
name: "uploader",
|
||||
title: () => enabledTools.uploader.name,
|
||||
route: "/prompt/uploader",
|
||||
component: UploaderPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < uploader.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
copyToProvider: get( current_settings ).hasOwnProperty( "copy-to-s3" ) ? get( current_settings )[ "copy-to-s3" ] : false,
|
||||
setInitialCopyToProvider: ( data ) => {
|
||||
if (
|
||||
get( location ) !== uploader.route &&
|
||||
!uploader.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "copy-to-s3" )
|
||||
) {
|
||||
uploader.copyToProvider = data.settings[ "copy-to-s3" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "not_offloaded" ) ||
|
||||
get( counts ).not_offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Copy to Provider turned on.
|
||||
if (
|
||||
data.changed_settings.includes( "copy-to-s3" ) &&
|
||||
get( current_settings ).hasOwnProperty( "copy-to-s3" ) &&
|
||||
uploader.copyToProvider !== get( current_settings )[ "copy-to-s3" ] &&
|
||||
get( current_settings )[ "copy-to-s3" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
uploader.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "copy-to-s3" ) &&
|
||||
uploader.copyToProvider !== get( current_settings )[ "copy-to-s3" ] &&
|
||||
get( current_settings )[ "copy-to-s3" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "copy-to-s3" ) ) {
|
||||
uploader.copyToProvider = get( current_settings )[ "copy-to-s3" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => uploader.isNextRoute( data ),
|
||||
"settings.save": ( data ) => uploader.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => uploader.setInitialCopyToProvider( data )
|
||||
}
|
||||
};
|
||||
pages.add( uploader );
|
||||
}
|
||||
|
||||
// Downloader tool prompt when Remove Local Files turned off.
|
||||
if ( enabledTools.hasOwnProperty( "downloader" ) ) {
|
||||
const downloader = {
|
||||
position: 450,
|
||||
name: "downloader",
|
||||
title: () => enabledTools.downloader.name,
|
||||
route: "/prompt/downloader",
|
||||
component: DownloaderPromptPage,
|
||||
onPreviousPage: () => {
|
||||
const previousPages = pages.withPrefix( "/prompt/" ).filter( ( page ) => page.position < downloader.position );
|
||||
|
||||
for ( const previousPage of previousPages ) {
|
||||
if ( get( location ) === previousPage.route ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
removeLocalFile: get( current_settings ).hasOwnProperty( "remove-local-file" ) ? get( current_settings )[ "remove-local-file" ] : false,
|
||||
setInitialRemoveLocalFile: ( data ) => {
|
||||
if (
|
||||
get( location ) !== downloader.route &&
|
||||
!downloader.onPreviousPage() &&
|
||||
data.hasOwnProperty( "settings" ) &&
|
||||
data.settings.hasOwnProperty( "remove-local-file" )
|
||||
) {
|
||||
downloader.removeLocalFile = data.settings[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isNextRoute: ( data ) => {
|
||||
// Anything to work with?
|
||||
if (
|
||||
!get( licence ).hasOwnProperty( "is_valid" ) ||
|
||||
!get( licence ).is_valid ||
|
||||
!get( current_settings ).hasOwnProperty( "bucket" ) ||
|
||||
!get( counts ).hasOwnProperty( "offloaded" ) ||
|
||||
get( counts ).offloaded < 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( data.hasOwnProperty( "changed_settings" ) ) {
|
||||
// Remove Local Files turned off.
|
||||
if (
|
||||
data.changed_settings.includes( "remove-local-file" ) &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
downloader.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
!get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Setting changed and event from previous prompt page.
|
||||
if (
|
||||
downloader.onPreviousPage() &&
|
||||
get( current_settings ).hasOwnProperty( "remove-local-file" ) &&
|
||||
downloader.removeLocalFile !== get( current_settings )[ "remove-local-file" ] &&
|
||||
!get( current_settings )[ "remove-local-file" ]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We're not interested in showing prompt, just ensure local state is up to date.
|
||||
// NOTE: This handles syncing the local state when moving on from this prompt too.
|
||||
if ( get( current_settings ).hasOwnProperty( "remove-local-file" ) ) {
|
||||
downloader.removeLocalFile = get( current_settings )[ "remove-local-file" ];
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
events: {
|
||||
"next": ( data ) => downloader.isNextRoute( data ),
|
||||
"settings.save": ( data ) => downloader.isNextRoute( data ),
|
||||
"page.initial.settings": ( data ) => downloader.setInitialRemoveLocalFile( data )
|
||||
}
|
||||
};
|
||||
pages.add( downloader );
|
||||
}
|
||||
}
|
||||
251
ui/pro/stores.js
Normal file
251
ui/pro/stores.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import {derived, get, writable} from "svelte/store";
|
||||
import {api, config, state} from "../js/stores";
|
||||
import {objectsDiffer} from "../js/objectsDiffer";
|
||||
|
||||
// Convenience readable store of licence, derived from config.
|
||||
// We currently have one licence applied to a plugin install.
|
||||
export const licence = derived( config, $config => $config.hasOwnProperty( "licences" ) ? $config.licences.at( 0 ) : [] );
|
||||
|
||||
// Convenience readable store of offload remaining with count message, derived from config.
|
||||
export const offloadRemainingWithCount = derived( config, $config => $config.offload_remaining_with_count );
|
||||
|
||||
// Convenience readable store of documentation, derived from config.
|
||||
export const documentation = derived( config, $config => $config.documentation );
|
||||
|
||||
/*
|
||||
* Tools.
|
||||
*/
|
||||
|
||||
// Whether tools are locked due to background activity such as upgrade.
|
||||
export const toolsLocked = writable( false );
|
||||
|
||||
// Keeps track of the currently running background tool.
|
||||
export const running = writable( "" );
|
||||
|
||||
const toolIcons = {
|
||||
"add-metadata": "offload",
|
||||
"reverse-add-metadata": "analyzerepair",
|
||||
"verify-add-metadata": "analyzerepair",
|
||||
"copy-buckets": "move",
|
||||
"download-and-remover": "remove",
|
||||
"downloader": "download",
|
||||
"elementor-analyze-and-repair": "analyzerepair",
|
||||
"move-objects": "move",
|
||||
"move-private-objects": "move",
|
||||
"move-public-objects": "move",
|
||||
"remove-local-files": "clean",
|
||||
"update-acls": "analyzerepair",
|
||||
"uploader": "offload",
|
||||
"woocommerce-product-urls": "analyzerepair",
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates store of tools info and API access methods.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createTools() {
|
||||
const { subscribe, set, update } = writable( {} );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
async action( tool, action ) {
|
||||
state.pausePeriodicFetch();
|
||||
|
||||
// Set the status text to the default busy description
|
||||
// until the API returns a calculated status description.
|
||||
tool.status_description = tool.busy_description;
|
||||
tool.short_status_description = tool.busy_description;
|
||||
|
||||
// Ensure all subscribers know the tool status is changing.
|
||||
update( _tools => {
|
||||
_tools[ tool.id ] = tool;
|
||||
|
||||
return _tools;
|
||||
} );
|
||||
|
||||
let result = {};
|
||||
const json = await api.put( "tools", {
|
||||
id: tool.id,
|
||||
action: action
|
||||
} );
|
||||
|
||||
if ( json.hasOwnProperty( "ok" ) ) {
|
||||
result = json;
|
||||
}
|
||||
|
||||
await state.resumePeriodicFetch();
|
||||
return result;
|
||||
},
|
||||
async start( tool ) {
|
||||
// Ensure all subscribers know that a tool is running.
|
||||
running.update( _running => tool.id );
|
||||
tool.is_queued = true;
|
||||
|
||||
return await this.action( tool, "start" );
|
||||
},
|
||||
async cancel( tool ) {
|
||||
tool.is_cancelled = true;
|
||||
|
||||
return await this.action( tool, "cancel" );
|
||||
},
|
||||
async pauseResume( tool ) {
|
||||
tool.is_paused = !tool.is_paused;
|
||||
|
||||
return await this.action( tool, "pause_resume" );
|
||||
},
|
||||
updateTools( json ) {
|
||||
if ( json.hasOwnProperty( "tools" ) ) {
|
||||
// Update our understanding of what the server's tools status is.
|
||||
update( _tools => {
|
||||
return { ...json.tools };
|
||||
} );
|
||||
|
||||
// Update our understanding of the currently running tool.
|
||||
const runningTool = Object.values( json.tools ).find( ( tool ) => tool.is_processing || tool.is_queued || tool.is_paused || tool.is_cancelled );
|
||||
|
||||
if ( runningTool ) {
|
||||
running.update( _running => runningTool.id );
|
||||
} else {
|
||||
running.update( _running => "" );
|
||||
}
|
||||
}
|
||||
},
|
||||
icon( tool, isRunning, animated ) {
|
||||
let icon = "tool-generic";
|
||||
let type = "default";
|
||||
|
||||
if ( isRunning ) {
|
||||
if ( tool.is_paused ) {
|
||||
type = "paused";
|
||||
} else if ( animated ) {
|
||||
type = "running-animated";
|
||||
} else {
|
||||
type = "active";
|
||||
}
|
||||
}
|
||||
|
||||
if ( tool && tool.hasOwnProperty( "slug" ) && toolIcons.hasOwnProperty( tool.slug ) ) {
|
||||
icon = "tool-" + toolIcons[ tool.slug ];
|
||||
}
|
||||
|
||||
if ( ["active", "default", "paused", "running-animated"].includes( type ) ) {
|
||||
icon = icon + "-" + type + ".svg";
|
||||
} else {
|
||||
icon = icon + "-default.svg";
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const tools = createTools();
|
||||
|
||||
/*
|
||||
* Assets.
|
||||
*/
|
||||
|
||||
// Does the app need a page refresh to resolve conflicts?
|
||||
export const assetsNeedsRefresh = writable( false );
|
||||
|
||||
// Whether assets settings are locked due to background activity such as upgrade.
|
||||
export const assetsSettingsLocked = writable( false );
|
||||
|
||||
// Convenience readable store of server's assets settings, derived from config.
|
||||
export const currentAssetsSettings = derived( config, $config => $config.assets_settings );
|
||||
|
||||
// Convenience readable store of defined assets settings keys, derived from config.
|
||||
export const assetsDefinedSettings = derived( config, $config => $config.assets_defined_settings );
|
||||
|
||||
// Convenience readable store of assets domain check info, derived from config.
|
||||
export const assetsDomainCheck = derived( config, $config => $config.assets_domain_check );
|
||||
|
||||
// Convenience readable store indicating whether Assets functionality may be used.
|
||||
export const enableAssets = derived( [licence, config], ( [$licence, $config] ) => {
|
||||
if (
|
||||
$licence.hasOwnProperty( "is_set" ) &&
|
||||
$licence.is_set &&
|
||||
$licence.hasOwnProperty( "is_valid" ) &&
|
||||
$licence.is_valid &&
|
||||
$config.hasOwnProperty( "assets_settings" )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} );
|
||||
|
||||
/**
|
||||
* Creates store of assets settings.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
function createAssetsSettings() {
|
||||
const { subscribe, set, update } = writable( [] );
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
async save() {
|
||||
const json = await api.put( "assets-settings", get( this ) );
|
||||
|
||||
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
||||
// Sync settings with what the server has.
|
||||
this.updateSettings( json );
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
reset() {
|
||||
set( { ...get( currentAssetsSettings ) } );
|
||||
},
|
||||
async fetch() {
|
||||
const json = await api.get( "assets-settings", {} );
|
||||
this.updateSettings( json );
|
||||
},
|
||||
updateSettings( json ) {
|
||||
if (
|
||||
json.hasOwnProperty( "assets_defined_settings" ) &&
|
||||
json.hasOwnProperty( "assets_settings" )
|
||||
) {
|
||||
const dirty = get( assetsSettingsChanged );
|
||||
const previousSettings = { ...get( currentAssetsSettings ) }; // cloned
|
||||
|
||||
// Update our understanding of what the server's settings are.
|
||||
config.update( _config => {
|
||||
return {
|
||||
..._config,
|
||||
assets_defined_settings: json.assets_defined_settings,
|
||||
assets_settings: json.assets_settings,
|
||||
};
|
||||
} );
|
||||
|
||||
// No need to check for changes from state if we've just saved these settings.
|
||||
if ( json.hasOwnProperty( "saved" ) && true === json.saved ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the settings weren't changed before, they shouldn't be now.
|
||||
if ( !dirty && get( assetsSettingsChanged ) ) {
|
||||
assetsSettings.reset();
|
||||
}
|
||||
|
||||
// If settings are in middle of being changed when changes come in
|
||||
// from server, reset to server version.
|
||||
if ( dirty && objectsDiffer( [previousSettings, get( currentAssetsSettings )] ) ) {
|
||||
assetsNeedsRefresh.update( _needsRefresh => true );
|
||||
assetsSettings.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const assetsSettings = createAssetsSettings();
|
||||
|
||||
// Have the assets settings been changed from current server side settings?
|
||||
export const assetsSettingsChanged = derived( [assetsSettings, currentAssetsSettings], objectsDiffer );
|
||||
67
ui/pro/toolSettingsNotifications.js
Normal file
67
ui/pro/toolSettingsNotifications.js
Normal file
@@ -0,0 +1,67 @@
|
||||
export const toolSettingsNotifications = {
|
||||
/**
|
||||
* Process local and server settings to return a new Map of inline notifications.
|
||||
*
|
||||
* @param {Map} notifications
|
||||
* @param {Object} settings
|
||||
* @param {Object} current_settings
|
||||
* @param {Object} strings
|
||||
* @param {Object} counts
|
||||
* @param {Object} licence
|
||||
*
|
||||
* @return {Map<string, Map<string, Object>>} keyed by setting name, containing map of notification objects keyed by id.
|
||||
*/
|
||||
process: ( notifications, settings, current_settings, strings, counts, licence ) => {
|
||||
// use-yearmonth-folders
|
||||
let entries = notifications.has( "use-yearmonth-folders" ) ? notifications.get( "use-yearmonth-folders" ) : new Map();
|
||||
if (
|
||||
current_settings.hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
current_settings[ "use-yearmonth-folders" ] &&
|
||||
settings.hasOwnProperty( "use-yearmonth-folders" ) &&
|
||||
!settings[ "use-yearmonth-folders" ] &&
|
||||
counts.hasOwnProperty( "offloaded" ) &&
|
||||
counts.offloaded > 0 &&
|
||||
licence.hasOwnProperty( "is_valid" ) &&
|
||||
licence.is_valid
|
||||
) {
|
||||
if ( !entries.has( "no-move-objects-year-month-notice" ) ) {
|
||||
entries.set( "no-move-objects-year-month-notice", {
|
||||
inline: true,
|
||||
type: "warning",
|
||||
message: strings.no_move_objects_year_month_notice
|
||||
} );
|
||||
}
|
||||
} else if ( entries.has( "no-move-objects-year-month-notice" ) ) {
|
||||
entries.delete( "no-move-objects-year-month-notice" );
|
||||
}
|
||||
|
||||
notifications.set( "use-yearmonth-folders", entries );
|
||||
|
||||
// object-versioning
|
||||
entries = notifications.has( "object-versioning" ) ? notifications.get( "object-versioning" ) : new Map();
|
||||
if (
|
||||
current_settings.hasOwnProperty( "object-versioning" ) &&
|
||||
current_settings[ "object-versioning" ] &&
|
||||
settings.hasOwnProperty( "object-versioning" ) &&
|
||||
!settings[ "object-versioning" ] &&
|
||||
counts.hasOwnProperty( "offloaded" ) &&
|
||||
counts.offloaded > 0 &&
|
||||
licence.hasOwnProperty( "is_valid" ) &&
|
||||
licence.is_valid
|
||||
) {
|
||||
if ( !entries.has( "no-move-objects-object-versioning-notice" ) ) {
|
||||
entries.set( "no-move-objects-object-versioning-notice", {
|
||||
inline: true,
|
||||
type: "warning",
|
||||
message: strings.no_move_objects_object_versioning_notice
|
||||
} );
|
||||
}
|
||||
} else if ( entries.has( "no-move-objects-object-versioning-notice" ) ) {
|
||||
entries.delete( "no-move-objects-object-versioning-notice" );
|
||||
}
|
||||
|
||||
notifications.set( "object-versioning", entries );
|
||||
|
||||
return notifications;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user