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:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

33
ui/pro/AssetsPage.svelte Normal file
View 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/>

View 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>
-->

View 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>

View 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>

View 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>

View 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}

View 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
View 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
View 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>

View 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>

View 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>

View 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
View 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
View 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>

View 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
View 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
View 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
View 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}

View 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
View 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>

View 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}

View 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
View 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>

View 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>

View 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
View 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
View 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 );

View 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;
}
};