diff --git a/docs/en/modules/admin/assets/images/components/proposals/component_02.png b/docs/en/modules/admin/assets/images/components/proposals/component_02.png index 6ef7f3f95f..f9f5f3d841 100644 Binary files a/docs/en/modules/admin/assets/images/components/proposals/component_02.png and b/docs/en/modules/admin/assets/images/components/proposals/component_02.png differ diff --git a/docs/en/modules/admin/assets/images/components/proposals/component_03.png b/docs/en/modules/admin/assets/images/components/proposals/component_03.png index 6c63098dae..a1a7fd3141 100644 Binary files a/docs/en/modules/admin/assets/images/components/proposals/component_03.png and b/docs/en/modules/admin/assets/images/components/proposals/component_03.png differ diff --git a/docs/en/modules/admin/pages/components/proposals.adoc b/docs/en/modules/admin/pages/components/proposals.adoc index 0810fd6f38..96788e4f54 100644 --- a/docs/en/modules/admin/pages/components/proposals.adoc +++ b/docs/en/modules/admin/pages/components/proposals.adoc @@ -59,17 +59,9 @@ image::components/proposals/component_02.png[Add component form] |=== |Field |Type |Description -|Scopes enabled +|Select filters for the component |Optional -|Check if you want to have Scopes filtering in this component. - -|Scopes enabled -|Optional -|Check if you want to have Scopes filtering available in this component. - -|Scope -|Optional -|Which xref:admin:settings/scopes.adoc[Scope] does this component belongs to. +|Select if you want to use filters in this component. |Vote limit per participant |Optional @@ -140,10 +132,6 @@ and the first image appears on the card. |Optional |Whether you want to be able to set permissions for every proposals. -|Collaborative drafts enabled -|Optional -|Check to enable xref:components/proposals/special_configurations/collaborative_drafts.adoc[collaborative drafts] in proposals. - |Participatory texts enabled |Optional |Check to enable xref:components/proposals/special_configurations/participatory_texts.adoc[participatory texts] in proposals. @@ -189,13 +177,13 @@ image::components/proposals/component_03.png[Add component form] |=== |Field |Type |Description -|Endorsements enabled +|Likes enabled |Optional |To enable participants to show public support for these proposals, with a "Like" button. -|Endorsements blocked +|Likes blocked |Optional -|Check for participants to be able to see the existing endorsements but not make any new one. +|Check for participants to be able to see the existing likes but not make any new one. |Votes enabled |Optional @@ -239,14 +227,6 @@ are shown sorted by random, and if the votes are blocked, then they are sorted b |Optional |A general announcement visible on the proposals list page for this phase. -|Hashtags added to all proposals -|Optional -|Automatically add hashtags to every proposal. - -|Hashtags suggested to participants for new proposals -|Optional -|Suggest hashtags to participants creating new proposals. Participants can select more than one hashtag. - |=== == Examples diff --git a/supplemental_ui/admin/adoc-control.js b/supplemental_ui/admin/adoc-control.js index fe3d01e941..886b206c73 100644 --- a/supplemental_ui/admin/adoc-control.js +++ b/supplemental_ui/admin/adoc-control.js @@ -149,10 +149,34 @@ window.Admin.AdocControl = (() => { this.commit(Transforms.insertLine(this.value(), this.textarea.selectionStart, text)); }, + // The Antora module whose images the field's media folder holds, so a picked + // image can be turned into the module-relative target Antora expects. + moduleMedia() { + const field = this.props.field; + const mediaFolder = (field?.get?.('media_folder') || '').replace(/\/+$/, ''); + const publicFolder = (field?.get?.('public_folder') || mediaFolder).replace(/\/+$/, ''); + const match = publicFolder.match(/\/modules\/([^/]+)\/assets\/images$/); + return { module: match ? match[1] : 'root' }; + }, + + // Convert a picked asset's public path to the target Antora resolves against + // the module's assets/images. A path in another module is prefixed with that + // module (`ROOT:` or `:`); anything else (e.g. an external URL) is + // used as is. + antoraImageTarget(value) { + const path = String(value); + const match = path.match(/\/modules\/([^/]+)\/assets\/images\/(.+)$/); + if (!match) return path; + const [, module, relative] = match; + const current = this.moduleMedia().module; + if (module.toLowerCase() === current.toLowerCase()) return relative; + return `${module}:${relative}`; + }, + // Open Sveltia's built-in file picker (existing file, upload, URL or stock - // photo). Every image is inserted with the canonical Antora - // `image::ROOT:[Alt text]` target, so the editor and the saved - // source stay consistent for existing and newly uploaded files alike. + // photo). Images are inserted with a module-relative Antora target such as + // `image::participants/foo.png[Alt text]`, so uploads stay in the current + // module's assets/images folder, consistent with the existing documentation. // // A fresh upload additionally registers a hidden upload reference. Sveltia // only uploads files whose blob: URL appears in the saved value, so the @@ -162,17 +186,23 @@ window.Admin.AdocControl = (() => { this.saveSelection(); const picked = await this.props.pickFile({ kind: 'image', multiple: false }); if (!picked) return; - const { fileName, refs } = this.resolvePickedImage(picked); - this.insertAtCursor(`image::ROOT:${fileName}[Alt text]`, refs); + const { target, refs } = this.resolvePickedImage(picked); + this.insertAtCursor(`image::${target}[Alt text]`, refs); }, - // A freshly uploaded file is returned with a blob: URL; an existing asset or - // external URL comes back as a public path. Only blob URLs need uploading. + // A freshly uploaded file is returned with a blob: URL and is stored at the + // module's images root, so it is referenced by file name. An existing asset + // or external URL comes back as a public path whose subfolder is preserved. + // Only blob URLs need uploading. resolvePickedImage(picked) { const isUpload = typeof picked.value === 'string' && picked.value.startsWith('blob:'); - const fileName = isUpload && picked.file ? picked.file.name : basename(picked.value); - const refs = isUpload ? [{ fileName, blobUrl: picked.value }] : []; - return { fileName, refs }; + const target = isUpload + ? picked.file + ? picked.file.name + : basename(picked.value) + : this.antoraImageTarget(picked.value); + const refs = isUpload ? [{ target, blobUrl: picked.value }] : []; + return { target, refs }; }, insertLink() { diff --git a/supplemental_ui/admin/adoc-preview.js b/supplemental_ui/admin/adoc-preview.js index 12e5d5638d..f11e84f40a 100644 --- a/supplemental_ui/admin/adoc-preview.js +++ b/supplemental_ui/admin/adoc-preview.js @@ -1,15 +1,13 @@ window.Admin = window.Admin || {}; // Antora-only constructs the in-browser renderer cannot resolve. Images are -// resolved per page: the module's images are built to en/develop// -// (and nested pages keep their subdirectory). ROOT-module images (the shared -// media folder) are referenced with `image::ROOT:` and resolve to -// en/develop/_images. +// resolved per page: image targets are module-relative and Antora builds them to +// en/develop//_images (the ROOT module maps to en/develop/_images). +// Legacy `image::ROOT:` targets from older drafts are still resolved. window.Admin.Preview = (() => { const { h, createClass } = window; const { strip, parse } = window.Admin.UploadBlock; - const sharedImagePath = 'docs/en/modules/ROOT/assets/images'; const processor = window.Asciidoctor ? window.Asciidoctor() : null; const COLLECTIONS = [ @@ -40,39 +38,70 @@ window.Admin.Preview = (() => { return `${base}/${dest}.html${fragment ? `#${fragment}` : ''}`; }; - // Resolve a stored ROOT image path to a URL Sveltia can serve in the admin. This matters because - // the site is edited through the editorial workflow: a freshly uploaded image lives on the draft - // branch, but the hard-coded built path (`/en/develop/_images/...`) only exists after publishing. - // The media API returns the draft asset, just like the built-in Image field preview does. - const resolveRootImageUrl = (getAsset, fileName) => { + // Repository folder holding a module's images, from which Antora builds the + // previewable `_images` path. The ROOT module directory is uppercase. + const moduleImageFolder = (module) => { + const dir = module === 'root' || module === 'ROOT' ? 'ROOT' : module; + return `docs/en/modules/${dir}/assets/images`; + }; + + // Built images directory Antora publishes a module's images under, regardless + // of the page's subdirectory. The ROOT module maps to the component root, so + // its images live at en/develop/_images rather than en/develop/root/_images. + const moduleImagesDir = (module) => { + const base = module === 'root' || module === 'ROOT' ? '/en/develop' : `/en/develop/${module}`; + return `${base}/_images`; + }; + + // Resolve a stored image path to a URL Sveltia can serve in the admin. This matters because the + // site is edited through the editorial workflow: a freshly uploaded image lives on the draft + // branch, but the hard-coded built path (`/en/develop/.../_images/...`) only exists after + // publishing. The media API returns the draft asset, just like the built-in Image field preview. + const resolveImageUrl = (getAsset, mediaFolder, target) => { if (typeof getAsset !== 'function') return ''; - const asset = getAsset(`/${sharedImagePath}/${fileName}`); + const asset = getAsset(`/${mediaFolder}/${target}`); const url = asset?.url ?? (typeof asset?.get === 'function' ? asset.get('url') : ''); return typeof url === 'string' ? url : ''; }; + // An image macro target, optionally prefixed with a module (`ROOT:` or + // `:`). The lookahead keeps external URLs such as `https://` out, and + // a URI scheme that survives it (e.g. `data:`) is left as is below. + const IMAGE_TARGET_PATTERN = /image::(?:([A-Za-z][\w-]*):(?!\/\/))?([^:\n\[\]]+)\[/g; + const URI_SCHEME = /^(?:https?|data|blob|file|mailto|tel|sms|cid|xmpp|ftp)$/i; + const renderAsciiDoc = (source, imagesDir, module = 'root', refs = [], getAsset) => { if (!processor) return ''; // Resolve a pending upload to its temporary blob URL and a committed one to the media URL - // Sveltia serves (so it previews even while the image is still on the draft branch). + // Sveltia serves (so it previews even while the image is still on the draft branch). A + // module-prefixed target (`ROOT:` or `:`) is resolved against that module; a bare + // target against the current module. Anything unresolved falls back to the built `_images` + // path Antora publishes the module's images under. const pending = new Map( refs .filter(({ blobUrl }) => blobUrl.startsWith('blob:')) - .map(({ fileName, blobUrl }) => [fileName, blobUrl]), - ); - const body = strip(source).replace( - /image::ROOT:([^\[\]\n]+)\[/g, - (_, fileName) => - `image::${pending.get(fileName) || resolveRootImageUrl(getAsset, fileName) || `ROOT:${fileName}`}[`, + .map(({ target, blobUrl }) => [target, blobUrl]), ); + const body = strip(source).replace(IMAGE_TARGET_PATTERN, (match, prefix, target) => { + if (prefix && URI_SCHEME.test(prefix)) return match; + // Key the pending lookup by the target as written, so a bare upload never + // stands in for another module's prefixed reference of the same name. Older + // drafts used `ROOT:` in the macro while storing the pending reference under + // the bare name, so fall back to that form for the ROOT module. + const key = prefix ? `${prefix}:${target}` : target; + const legacy = prefix && prefix.toUpperCase() === 'ROOT' ? pending.get(target) : undefined; + const mod = prefix || module; + const url = + pending.get(key) || legacy || resolveImageUrl(getAsset, moduleImageFolder(mod), target); + return `image::${url || `${moduleImagesDir(mod)}/${target}`}[`; + }); const previewable = body .replace(/^include::[^\n]*$/gm, '') .replace( /xref:([^\[\]]+)\[([^\]]*)\]/g, (_, target, label) => `link:${resolveXref(target, module)}[${label || target}]`, - ) - .replace(/image::ROOT:([^\[\]]+)/g, 'image::/en/develop/_images/$1'); + ); return processor.convert(previewable, { safe: 'safe', attributes: { imagesdir: imagesDir, showtitle: true }, @@ -93,16 +122,6 @@ window.Admin.Preview = (() => { return ''; }; - // Build the images dir for a page. Antora publishes a module's images under - // its module-level `_images` directory regardless of the page's subdirectory, - // so image macros such as `image::spaces/processes/foo.png` resolve from there. - const imagesDirForEntry = (module) => { - // The ROOT module maps to the component root, so its images live at - // en/develop/_images rather than en/develop/root/_images. - const base = module === 'root' ? '/en/develop' : `/en/develop/${module}`; - return `${base}/_images`; - }; - // Approximate, instant preview. For an exact Antora build use the "View // Preview" deploy preview link provided by the backend. const makeDocPreview = (module) => @@ -111,13 +130,7 @@ window.Admin.Preview = (() => { const source = this.props.entry.getIn(['data', 'body']) || ''; const { refs } = parse(source); const html = sanitizeHtml( - renderAsciiDoc( - source, - imagesDirForEntry(module), - module, - refs, - this.props.getAsset, - ), + renderAsciiDoc(source, moduleImagesDir(module), module, refs, this.props.getAsset), ); if (html) { return h('div', { diff --git a/supplemental_ui/admin/config.yml b/supplemental_ui/admin/config.yml index d589f06229..b03b93fb05 100644 --- a/supplemental_ui/admin/config.yml +++ b/supplemental_ui/admin/config.yml @@ -38,6 +38,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/ROOT/assets/images + public_folder: /docs/en/modules/ROOT/assets/images - name: admin label: Admin label_singular: Page @@ -61,6 +63,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/admin/assets/images + public_folder: /docs/en/modules/admin/assets/images - name: contribute label: Contribute label_singular: Page @@ -80,6 +84,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/contribute/assets/images + public_folder: /docs/en/modules/contribute/assets/images - name: features label: Features label_singular: Page @@ -99,6 +105,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/features/assets/images + public_folder: /docs/en/modules/features/assets/images - name: publications label: Publications label_singular: Page @@ -118,6 +126,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/publications/assets/images + public_folder: /docs/en/modules/publications/assets/images - name: releases label: Releases label_singular: Page @@ -137,6 +147,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/releases/assets/images + public_folder: /docs/en/modules/releases/assets/images - name: understand label: Understand label_singular: Page @@ -156,6 +168,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/understand/assets/images + public_folder: /docs/en/modules/understand/assets/images - name: whitepaper label: Whitepaper label_singular: Page @@ -175,6 +189,8 @@ collections: - name: body label: AsciiDoc widget: adoc + media_folder: /docs/en/modules/whitepaper/assets/images + public_folder: /docs/en/modules/whitepaper/assets/images # The site navigation lives in a single AsciiDoc file rather than a folder of # entries. The custom `adoc` format keeps the whole file in `body`; the diff --git a/supplemental_ui/admin/upload-block.js b/supplemental_ui/admin/upload-block.js index 692a607375..0806e8c956 100644 --- a/supplemental_ui/admin/upload-block.js +++ b/supplemental_ui/admin/upload-block.js @@ -14,7 +14,7 @@ window.Admin.UploadBlock = (() => { const serialize = (refs) => refs.length ? `\n${UPLOAD_REF}\n${refs - .map(({ fileName, blobUrl }) => `// image:${fileName} blob:${blobUrl}`) + .map(({ target, blobUrl }) => `// image:${target} blob:${blobUrl}`) .join('\n')}` : ''; @@ -24,7 +24,7 @@ window.Admin.UploadBlock = (() => { block.split('\n').forEach((line) => { const match = line.match(uploadLinePattern); - if (match) refs.push({ fileName: match[1], blobUrl: match[2] }); + if (match) refs.push({ target: match[1], blobUrl: match[2] }); }); return { body: strip(text), refs };