fix: fetch image resource with auth params (#1757)

* fix: fetch image resource with auth params

* chore: refactor to pre-load the query params

* chore: correct non-async

* chore: fix mock of auth

* chore: qlik/api 1.37.0
This commit is contained in:
Tobias Åström
2025-06-24 12:01:31 +02:00
committed by GitHub
parent f2554260d6
commit 388730565e
16 changed files with 100 additions and 55 deletions

View File

@@ -13,6 +13,7 @@
"@nebula.js/supernova": "^5.17.0",
"@nebula.js/theme": "^5.17.0",
"@nebula.js/ui": "^5.17.0",
"@qlik/api": "1.37.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"extend": "3.0.2",

View File

@@ -10,6 +10,12 @@ import * as flagsModule from '../flags/flags';
import * as appThemeModule from '../app-theme';
import * as deviceTypeModule from '../device-type';
jest.mock('@qlik/api/auth', () => ({
getWebResourceAuthParams: async () => ({
queryParams: {},
}),
}));
describe('nucleus', () => {
let createObjectMock;
let getObjectMock;
@@ -28,7 +34,7 @@ describe('nucleus', () => {
setThemeMock = jest.fn();
appThemeFnMock = jest.fn().mockReturnValue({ externalAPI: 'external', setTheme: setThemeMock });
deviceTypeFnMock = jest.fn().mockReturnValue('desktop');
rootAppMock = jest.fn().mockReturnValue([{}]);
rootAppMock = jest.fn().mockReturnValue([{ context: () => {} }]);
translatorAddMock = jest.fn();
translatorLanguageMock = jest.fn();
translator = { add: translatorAddMock, language: translatorLanguageMock, hi: 'hi' };

View File

@@ -356,6 +356,7 @@ const Cell = forwardRef(
externalFocusManagement,
disableCellPadding = false,
navigation: navigationApi,
queryParams,
} = useContext(InstanceContext);
const [internalEmitter] = useState(emitter || createEmitter);
const theme = useTheme();
@@ -377,6 +378,7 @@ const Cell = forwardRef(
app: halo.app,
themeName,
disableThemeBorder: snOptions?.disableThemeBorder,
queryParams,
});
const isRtl = !!(snOptions?.direction === 'rtl');

View File

@@ -9,7 +9,7 @@ import ListBox from './ListBox';
import InstanceContext from '../../contexts/InstanceContext';
import ListBoxSearch from './components/ListBoxSearch';
import getListboxContainerKeyboardNavigation from './interactions/keyboard-navigation/keyboard-nav-container';
import getStyles from './assets/styling';
import useListboxStyling from './assets/styling';
import useAppSelections from '../../hooks/useAppSelections';
import createSelectionState from './hooks/selections/selectionState';
import { DENSE_ROW_HEIGHT, SCROLL_BAR_WIDTH } from './constants';
@@ -85,10 +85,10 @@ function ListBoxInline({ options, layout }) {
const theme = useTheme();
const { translator, keyboardNavigation, themeApi, constraints } = useContext(InstanceContext);
const { translator, keyboardNavigation, themeApi, queryParams, constraints } = useContext(InstanceContext);
const { checkboxes = checkboxesOption } = layout || {};
const styles = getStyles({ app, themeApi, theme, components, checkboxes });
const styles = useListboxStyling({ app, themeApi, theme, queryParams, components, checkboxes });
const isDirectQuery = isDirectQueryEnabled({ appLayout: app?.layout });
@@ -189,7 +189,7 @@ function ListBoxInline({ options, layout }) {
const isRtl = direction === 'rtl';
if (!model || !layout || !translator) {
if (!model || !layout || !translator || !styles) {
return null;
}

View File

@@ -20,7 +20,7 @@ import ListBoxSearch from './components/ListBoxSearch';
import useObjectSelections from '../../hooks/useObjectSelections';
import createSelectionState from './hooks/selections/selectionState';
import getHasSelections from './assets/has-selections';
import getStyles from './assets/styling';
import useListboxStyling from './assets/styling';
import useTempKeyboard from './components/useTempKeyboard';
export default function ListBoxPopover({
@@ -93,7 +93,7 @@ export default function ListBoxPopover({
model.unlock('/qListObjectDef');
}, [model]);
const { translator, themeApi, keyboardNavigation } = useContext(InstanceContext);
const { translator, themeApi, keyboardNavigation, hostConfig } = useContext(InstanceContext);
const moreAlignTo = useRef();
const containerRef = useRef();
const [selections] = useObjectSelections(app, model, containerRef);
@@ -102,7 +102,7 @@ export default function ListBoxPopover({
const keyboard = useTempKeyboard({ containerRef, enabled: keyboardNavigation });
const { checkboxes = checkboxesOption } = layout || {};
const styles = getStyles({ themeApi, theme, components, checkboxes });
const styles = useListboxStyling({ themeApi, theme, hostConfig, components, checkboxes });
useEffect(() => {
if (selections && open) {
@@ -112,7 +112,7 @@ export default function ListBoxPopover({
}
}, [selections, open]);
if (!model || !layout || !translator) {
if (!model || !layout || !translator || !styles) {
return null;
}

View File

@@ -1,4 +1,4 @@
import getStyling, { DEFAULT_SELECTION_COLORS, getOverridesAsObject } from '../styling';
import { getStyles, DEFAULT_SELECTION_COLORS, getOverridesAsObject } from '../styling';
describe('styling', () => {
let theme = {};
@@ -81,23 +81,23 @@ describe('styling', () => {
},
];
});
it('background color expression should be used', () => {
const styles = getStyling({ app, themeApi, theme, components });
it('background color expression should be used', async () => {
const styles = await getStyles({ app, themeApi, theme, components });
expect(styles.background.backgroundColor).toEqual('="some expression"');
});
it('background color expression should NOT be used, but instead color picker color', () => {
it('background color expression should NOT be used, but instead color picker color', async () => {
components[0].background.useExpression = false;
const styles = getStyling({ app, themeApi, theme, components });
const styles = await getStyles({ app, themeApi, theme, components });
expect(styles.background.backgroundColor).toEqual('#hex-color');
});
it('background image should be exposed', () => {
it('background image should be exposed', async () => {
components[0].background.image = {
mode: 'media',
mediaUrl: { qStaticContentUrl: { qUrl: 'some-image.png' } },
qStaticContentUrl: {},
sizing: 'stretchFit',
};
const styles = getStyling({ app, themeApi, theme, components });
const styles = await getStyles({ app, themeApi, theme, components });
expect(styles.background.backgroundImage).toEqual("url('https://hey-heysome-image.png')");
expect(styles.background.backgroundRepeat).toEqual('no-repeat');
expect(styles.background.backgroundSize).toEqual('100% 100%');
@@ -105,21 +105,21 @@ describe('styling', () => {
});
});
it('search - should get its color from theme style', () => {
const styles = getStyling({ app, themeApi, theme, components: [] });
it('search - should get its color from theme style', async () => {
const styles = await getStyles({ app, themeApi, theme, components: [] });
expect(styles.search.color).toEqual('object.listBox,content,color');
});
it('search - should get desired color if contrasting enough', () => {
it('search - should get desired color if contrasting enough', async () => {
themeApi.getStyle = () => '#888888';
const styles = getStyling({ app, themeApi, theme, components: [] });
const styles = await getStyles({ app, themeApi, theme, components: [] });
expect(styles.search.color).toEqual('#888888');
});
it('search - should get a better contrasting color if not good contrast against white', () => {
it('search - should get a better contrasting color if not good contrast against white', async () => {
themeApi.getStyle = () => '#aaa';
const styles = getStyling({ app, themeApi, theme, components: [] });
const styles = await getStyles({ app, themeApi, theme, components: [] });
expect(styles.search.color).toEqual('#000000');
});
it('header', () => {
it('header', async () => {
components = [
{
key: 'theme',
@@ -133,18 +133,18 @@ describe('styling', () => {
];
let inst;
let header;
inst = getStyling({ app, themeApi, theme, components: [] });
inst = await getStyles({ app, themeApi, theme, components: [] });
header = inst.header;
expect(header.fontSize).toEqual('object.listBox,title.main,fontSize');
expect(header.color).toEqual('object.listBox,title.main,color');
inst = getStyling({ app, themeApi, theme, components });
inst = await getStyles({ app, themeApi, theme, components });
header = inst.header;
expect(header.fontSize).toEqual('size-from-component');
expect(header.color).toEqual('color-from-component');
});
it('content - should override text color with a contrasting color since we have specified a text color and useContrastColor is true', () => {
it('content - should override text color with a contrasting color since we have specified a text color and useContrastColor is true', async () => {
components = [
{
key: 'theme',
@@ -163,11 +163,11 @@ describe('styling', () => {
themeApi.getStyle = (ns, path, prop) => (prop.includes('possible') ? POSSIBLE_COLOR : `${ns},${path},${prop}`);
components[0].content.fontColor.color = '#FFFFFF';
const styles2 = getStyling({ app, themeApi, theme, components });
const styles2 = await getStyles({ app, themeApi, theme, components });
expect(styles2.content.color).toEqual(CONTRASTING_TO_POSSIBLE);
});
it('content - should override with component properties', () => {
it('content - should override with component properties', async () => {
components = [
{
key: 'theme',
@@ -181,7 +181,7 @@ describe('styling', () => {
},
},
];
const styles = getStyling({ app, themeApi, theme, components });
const styles = await getStyles({ app, themeApi, theme, components });
const { content } = styles;
expect(content.fontSize).toEqual('size-from-component');
expect(content.color).toEqual('color-from-component');
@@ -203,10 +203,12 @@ describe('styling', () => {
themeApi.getStyle = () => undefined;
});
const getStylingCaller = () =>
getStyling({ app, themeApi, theme, components, themeSelectionColorsEnabled }).selections;
const getStylingCaller = async () => {
const res = await getStyles({ app, themeApi, theme, components, themeSelectionColorsEnabled });
return res.selections;
};
it('should return selection colors from components', () => {
it('should return selection colors from components', async () => {
components = [
{
key: 'selections',
@@ -230,7 +232,8 @@ describe('styling', () => {
},
];
expect(getStylingCaller()).toMatchObject({
const res = await getStylingCaller();
expect(res).toMatchObject({
selected: 'selected-from-component',
alternative: 'alternative-from-component',
excluded: 'excluded-from-component',
@@ -239,10 +242,11 @@ describe('styling', () => {
});
});
it('should return selection colors from themeAPI', () => {
it('should return selection colors from themeAPI', async () => {
themeApi.getStyle = (ns, path, prop) => `${ns},${path},${prop}`;
expect(getStylingCaller()).toMatchObject({
const res = await getStylingCaller();
expect(res).toMatchObject({
selected: 'object.listBox,,dataColors.selected',
alternative: 'object.listBox,,dataColors.alternative',
excluded: 'object.listBox,,dataColors.excluded',
@@ -250,10 +254,11 @@ describe('styling', () => {
possible: 'object.listBox,,dataColors.possible',
});
});
it('should return selection colors from mui theme, except possible which returns background color', () => {
it('should return selection colors from mui theme, except possible which returns background color', async () => {
themeApi.getStyle = (ns, path, prop) => (prop === 'backgroundColor' ? `${ns},${path},${prop}` : undefined);
expect(getStylingCaller()).toMatchObject({
const res = await getStylingCaller();
expect(res).toMatchObject({
selected: 'selected-from-theme',
alternative: 'alternative-from-theme',
excluded: 'excluded-from-theme',
@@ -262,8 +267,9 @@ describe('styling', () => {
});
});
it('should return selection colors from mui theme, for all states', () => {
expect(getStylingCaller()).toMatchObject({
it('should return selection colors from mui theme, for all states', async () => {
const res = await getStylingCaller();
expect(res).toMatchObject({
selected: 'selected-from-theme',
alternative: 'alternative-from-theme',
excluded: 'excluded-from-theme',
@@ -272,10 +278,10 @@ describe('styling', () => {
});
});
it('should return selection colors from hardcoded default', () => {
it('should return selection colors from hardcoded default', async () => {
theme.palette.selected = {};
expect(getStylingCaller()).toMatchObject(DEFAULT_SELECTION_COLORS);
const res = await getStylingCaller();
expect(res).toMatchObject(DEFAULT_SELECTION_COLORS);
});
});
});

View File

@@ -90,7 +90,7 @@ function getSearchBGColor(bgCol, getListboxStyle) {
return searchBgColorObj.isInvalid() ? bgCol : searchBgColorObj.getRGBA();
}
export default function getStyles({ app, themeApi, theme, components = [], checkboxes = false }) {
export function getStyles({ app, themeApi, theme, queryParams, components = [], checkboxes = false }) {
const overrides = getOverridesAsObject(components);
const getListboxStyle = (path, prop) => themeApi.getStyle('object.listBox', path, prop);
const getColorPickerColor = (c) => (c?.index > 0 || c?.color ? themeApi.getColorPickerColor(c) : undefined);
@@ -109,7 +109,7 @@ export default function getStyles({ app, themeApi, theme, components = [], check
const bgComponentColor = getBackgroundColor({ themeApi, themeOverrides });
const bgImage = themeOverrides.background?.image
? resolveBgImage({ bgImage: themeOverrides.background.image }, app)
? resolveBgImage({ bgImage: themeOverrides.background.image }, app, queryParams)
: undefined;
const bgColor = bgComponentColor || getListboxStyle('', 'backgroundColor') || theme.palette.background.default;
@@ -171,3 +171,7 @@ export default function getStyles({ app, themeApi, theme, components = [], check
selections,
};
}
export default function useListboxStyling({ app, themeApi, theme, queryParams, components, checkboxes }) {
return getStyles({ app, themeApi, theme, queryParams, components, checkboxes });
}

View File

@@ -9,4 +9,6 @@ export default React.createContext({
themeApi: null,
modelStore: {},
selectionStore: {},
hostConfig: null,
queryParams: null,
});

View File

@@ -35,7 +35,7 @@ const getThemeObjectType = (visualization) => {
return visualization;
};
const useStyling = ({ layout, theme, app, themeName, disableThemeBorder }) => {
const useStyling = ({ layout, theme, app, themeName, disableThemeBorder, queryParams }) => {
const styling = useMemo(() => {
if (layout && theme) {
const generalComp = layout.components ? layout.components.find((comp) => comp.key === 'general') : null;
@@ -46,7 +46,7 @@ const useStyling = ({ layout, theme, app, themeName, disableThemeBorder }) => {
subTitle: resolveTextStyle(generalComp, 'subTitle', theme, objectType),
};
const bgColor = resolveBgColor(generalComp, theme, objectType);
const bgImage = resolveBgImage(generalComp, app);
const bgImage = resolveBgImage(generalComp, app, queryParams);
const border = resolveBorder(generalComp, theme, objectType, disableThemeBorder);
const borderRadius = resolveBorderRadius(generalComp, theme, objectType);
const boxShadow = resolveBoxShadow(generalComp, theme, objectType);

View File

@@ -1,4 +1,5 @@
/* eslint no-underscore-dangle:0 */
import auth from '@qlik/api/auth';
import React from 'react';
import appLocaleFn from './locale/app-locale';
import appThemeFn from './app-theme';
@@ -221,6 +222,7 @@ function nuked(configuration = {}) {
let currentContext = {
...configuration.context,
translator: locale.translator,
hostConfig: configuration.hostConfig,
};
const [root, modelStore] = bootNebulaApp({
@@ -280,7 +282,18 @@ function nuked(configuration = {}) {
)
);
let currentThemePromise = appTheme.setTheme(configuration.context.theme);
let currentSetupPromise = new Promise((resolve) => {
const p = async () => {
await appTheme.setTheme(configuration.context.theme);
if (configuration.hostConfig && auth && auth.getWebResourceAuthParams) {
const { queryParams } = await auth.getWebResourceAuthParams({ hostConfig: configuration.hostConfig });
currentContext.queryParams = queryParams;
root.context(currentContext);
}
};
p().then(resolve);
});
let selectionsApi = null;
let selectionsComponentReference = null;
@@ -328,7 +341,7 @@ function nuked(configuration = {}) {
* });
*/
render: async (cfg) => {
await currentThemePromise;
await currentSetupPromise;
if (cfg.id) {
return get(cfg, halo, modelStore);
}
@@ -399,8 +412,9 @@ function nuked(configuration = {}) {
};
if (changes.theme) {
currentThemePromise = appTheme.setTheme(changes.theme);
await currentThemePromise;
await currentSetupPromise;
currentSetupPromise = appTheme.setTheme(changes.theme);
await currentSetupPromise;
}
if (changes.language) {

View File

@@ -62,16 +62,24 @@ function resolveImageUrl(app, relativeUrl) {
return relativeUrl ? getSenseServerUrl(app) + relativeUrl : undefined;
}
export function resolveBgImage(bgComp, app) {
export function resolveBgImage(bgComp, app, queryParams) {
const bgImageDef = bgComp?.bgImage;
if (bgImageDef) {
let url = '';
if (bgImageDef.mode === 'media' || bgComp.useImage === 'media') {
let authParamsAsString;
const urlObj = bgImageDef?.mediaUrl;
const { qUrl } = urlObj?.qStaticContentUrl || {};
url = qUrl ? decodeURIComponent(qUrl) : undefined;
url = resolveImageUrl(app, url);
if (queryParams) {
authParamsAsString = Object.entries(queryParams)
.map(([key, value]) => `&${key}=${encodeURIComponent(value)}`)
.join('&');
url = `${url}?${authParamsAsString}`;
}
}
if (bgImageDef.mode === 'expression') {
url = bgImageDef.expressionUrl ? decodeURIComponent(bgImageDef.expressionUrl) : undefined;

View File

@@ -52,7 +52,7 @@
"@nebula.js/supernova": "^5.17.0",
"@nebula.js/theme": "^5.17.0",
"@nebula.js/ui": "^5.17.0",
"@qlik/api": "^1.37.0",
"@qlik/api": "1.37.0",
"@scriptappy/cli": "0.10.0",
"@scriptappy/from-jsdoc": "0.19.0",
"@scriptappy/to-dts": "1.0.0",

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@nebula.js/stardust": "<%= nebulaVersion %>",
"@nebula.js/sn-bar-chart": "^1.x",
"@qlik/api": "^1.17.0",
"@qlik/api": "^1.37.0",
"enigma.js": "^2.6.3",
"parcel": "2.13.3"
}

View File

@@ -54,7 +54,7 @@ const config = {
],
coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
reporters: ['default', ['jest-junit', { outputDirectory: 'coverage/junit/' }]],
transformIgnorePatterns: ['/node_modules/(?!@qlik/sdk)'],
transformIgnorePatterns: ['/node_modules/(?!@qlik/sdk|@qlik/api)'],
moduleNameMapper: {
'd3-color': '<rootDir>/node_modules/d3-color/dist/d3-color.min.js',
},

View File

@@ -132,6 +132,7 @@ const config = ({ format = 'umd', debug = false, file, targetPkg }) => {
sourcemap: true,
banner,
globals,
inlineDynamicImports: true,
},
external,
plugins: [
@@ -144,6 +145,7 @@ const config = ({ format = 'umd', debug = false, file, targetPkg }) => {
}),
nodeResolve({
extensions: [debug ? '.dev.js' : false, '.js', '.jsx'].filter(Boolean),
browser: true,
}),
json(),
commonjs(),

View File

@@ -3425,7 +3425,7 @@
tar-fs "^3.0.8"
yargs "^17.7.2"
"@qlik/api@^1.37.0":
"@qlik/api@1.37.0":
version "1.37.0"
resolved "https://registry.yarnpkg.com/@qlik/api/-/api-1.37.0.tgz#70c093ba5c5e966cb9d37bc6a9e1509fe6a9ef7e"
integrity sha512-sVzCEXLt7htufJQuqxXsGdUyqFFd5wslLHfXlpDcA18Nx4nfF9f9HHTNRQU1mdVMLTXn2UlR2wjBPNMM7HXSLw==