Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 5 additions & 25 deletions docs/en/modules/admin/pages/components/proposals.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
|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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 40 additions & 10 deletions supplemental_ui/admin/adoc-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<module>:`); 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:<filename>[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
Expand All @@ -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() {
Expand Down
87 changes: 50 additions & 37 deletions supplemental_ui/admin/adoc-preview.js
Original file line number Diff line number Diff line change
@@ -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/<module>/
// (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/<module>/_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 = [
Expand Down Expand Up @@ -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
// `<module>:`). 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 `<module>:`) 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;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
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 },
Expand All @@ -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) =>
Expand All @@ -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', {
Expand Down
16 changes: 16 additions & 0 deletions supplemental_ui/admin/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions supplemental_ui/admin/upload-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`
: '';

Expand All @@ -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 };
Expand Down
Loading