Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N8n 3623 welcome experience #3289

Merged
merged 48 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e83f652
✨ Injecting a welcome sticky note if a corresponding flag has been re…
MiloradFilipovic May 10, 2022
9fd6d36
🔒 Allowing resources from `/static` route to be displayed in markown …
MiloradFilipovic May 10, 2022
5cfb4f0
✨ Implemented image width control via markdown URLs
MiloradFilipovic May 10, 2022
17aa9d6
💄Updating quickstart video thumbnail images.
MiloradFilipovic May 10, 2022
ab3ceb5
🔨 Updated new workflow action name and quickstart sticky name
MiloradFilipovic May 11, 2022
6db732b
✨ Added quickstart menu item in the Help menu
MiloradFilipovic May 11, 2022
805c954
🔨 Moving quickstart video thumbnail to the translation file.
MiloradFilipovic May 11, 2022
9bdf33f
🔒 Limiting http static resource requests in markdown img tags only to…
MiloradFilipovic May 11, 2022
2906a99
🔒 Adding more file types to supported image list in markown component.
MiloradFilipovic May 11, 2022
6f30b2e
👌 Extracting quickstart note name to constant.
MiloradFilipovic May 11, 2022
c5abdc8
Merge pull request #3276 from n8n-io/n8n-3619-fe-inject-welcome-sticky
MiloradFilipovic May 11, 2022
d9c81eb
🐘 add DB migration sqlite
BHesseldieck May 13, 2022
9201b9b
⚡️ add logic for onboarding flow flag
BHesseldieck May 13, 2022
c534c5e
🐘 add postgres migration for user settings
BHesseldieck May 13, 2022
06e3823
🐘 add mysql migration for user settings
BHesseldieck May 13, 2022
d7507bf
✨ Injecting a welcome sticky note if a corresponding flag has been re…
MiloradFilipovic May 10, 2022
0426283
🔒 Allowing resources from `/static` route to be displayed in markown …
MiloradFilipovic May 10, 2022
446b43c
✨ Implemented image width control via markdown URLs
MiloradFilipovic May 10, 2022
50d20a2
💄Updating quickstart video thumbnail images.
MiloradFilipovic May 10, 2022
4ee239e
🔨 Updated new workflow action name and quickstart sticky name
MiloradFilipovic May 11, 2022
812612b
✨ Added quickstart menu item in the Help menu
MiloradFilipovic May 11, 2022
09b6dcf
🔨 Moving quickstart video thumbnail to the translation file.
MiloradFilipovic May 11, 2022
58295e7
🔒 Limiting http static resource requests in markdown img tags only to…
MiloradFilipovic May 11, 2022
92cef76
🔒 Adding more file types to supported image list in markown component.
MiloradFilipovic May 11, 2022
2af4145
👌 Extracting quickstart note name to constant.
MiloradFilipovic May 11, 2022
111672a
📈 Added telemetry events to quickstart sticky note.
MiloradFilipovic May 12, 2022
549aab5
⚡ Disable sticky node type from showing in expression editor
MiloradFilipovic May 12, 2022
5aea6af
Merge pull request #3276 from n8n-io/n8n-3619-fe-inject-welcome-sticky
MiloradFilipovic May 11, 2022
cef3d8a
🔨 Improving welcome video link detecton when triggering telemetry events
MiloradFilipovic May 12, 2022
ced8d42
👌Moved sticky links click handling logic outside of the design system…
MiloradFilipovic May 12, 2022
0d2a6c1
👌Improving sticky note link telemetry tracking.
MiloradFilipovic May 13, 2022
aa4c05f
🔨 Refactoring markdown component click event logic.
MiloradFilipovic May 13, 2022
01859c3
🔨 Moving bits of clicked link detection logic to Markdown component.
MiloradFilipovic May 13, 2022
c395bf2
💄Fixing code spacing.
MiloradFilipovic May 13, 2022
6bb83cf
Merge pull request #3286 from n8n-io/N8N-3621-skip-sticky-notes-in-ex…
mutdmour May 13, 2022
c1d7500
Merge pull request #3285 from n8n-io/N8N-3624-welcome-experience-tele…
MiloradFilipovic May 13, 2022
2c62160
Merge branch 'n8n-3623-welcome-experience' of github.com:n8n-io/n8n i…
mutdmour May 16, 2022
bb549d2
merge in branch
mutdmour May 16, 2022
d22ebed
remove transpileonly option
mutdmour May 16, 2022
303b5e9
update package lock
mutdmour May 16, 2022
16a3c78
💄Changing the default route to `/workflow`, updating welcome sticky c…
MiloradFilipovic May 16, 2022
9a593b1
Merge pull request #3299 from n8n-io/N8N-3623-welcome-experience-feed…
mutdmour May 16, 2022
7df4e7c
Merge pull request #3291 from n8n-io/n8n-3618-be-set-flag-that-user-h…
mutdmour May 16, 2022
850f8a5
Merge branch 'master' of github.com:n8n-io/n8n into n8n-3623-welcome-…
mutdmour May 16, 2022
cb504fe
remove hardcoded
mutdmour May 16, 2022
bbfeb44
🐛 Fixing the onboarding threshold logic so sticky notes are skipped w…
MiloradFilipovic May 16, 2022
9b58d01
Merge pull request #3300 from n8n-io/n8n-3623-onboarding-threshold-lo…
mutdmour May 16, 2022
c17407f
👕 Fixing linting errors.
MiloradFilipovic May 16, 2022
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
114,961 changes: 10,193 additions & 104,768 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/cli/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ export const schema = {
default: 'My workflow',
env: 'WORKFLOWS_DEFAULT_NAME',
},
onboardingFlowDisabled: {
doc: 'Show onboarding flow in new workflow',
format: 'Boolean',
default: false,
env: 'N8N_ONBOARDING_FLOW_DISABLED',
},
},

executions: {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/GenericHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export async function generateUniqueName(

// name is unique
if (found.length === 0) {
return { name: requestedName };
return requestedName;
}

const maxSuffix = found.reduce((acc, { name }) => {
Expand All @@ -190,10 +190,10 @@ export async function generateUniqueName(

// name is duplicate but no numeric suffixes exist yet
if (maxSuffix === 0) {
return { name: `${requestedName} 2` };
return `${requestedName} 2`;
}

return { name: `${requestedName} ${maxSuffix + 1}` };
return `${requestedName} ${maxSuffix + 1}`;
}

export async function validateEntity(
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,10 @@ export interface IPersonalizationSurveyAnswers {
workArea: string[] | string | null;
}

export interface IUserSettings {
isOnboarded?: boolean;
}

export interface IUserManagementSettings {
enabled: boolean;
showSetupOnFirstLoad?: boolean;
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ import * as TagHelpers from './TagHelpers';
import { InternalHooksManager } from './InternalHooksManager';
import { TagEntity } from './databases/entities/TagEntity';
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
import { getSharedWorkflowIds, whereClause } from './WorkflowHelpers';
import { getSharedWorkflowIds, isBelowOnboardingThreshold, whereClause } from './WorkflowHelpers';
import { getCredentialTranslationPath, getNodeTranslationPath } from './TranslationHelpers';
import { WEBHOOK_METHODS } from './WebhookHelpers';

Expand Down Expand Up @@ -911,7 +911,14 @@ class App {
const requestedName =
req.query.name && req.query.name !== '' ? req.query.name : this.defaultWorkflowName;

return await GenericHelpers.generateUniqueName(requestedName, 'workflow');
const name = await GenericHelpers.generateUniqueName(requestedName, 'workflow');

const onboardingFlowEnabled =
!config.getEnv('workflows.onboardingFlowDisabled') &&
!req.user.settings?.isOnboarded &&
(await isBelowOnboardingThreshold(req.user));

return { name, onboardingFlowEnabled };
}),
);

Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-param-reassign */
import { In } from 'typeorm';
import {
IDataObject,
IExecuteData,
Expand Down Expand Up @@ -596,3 +597,52 @@ export async function getSharedWorkflowIds(user: User): Promise<number[]> {

return sharedWorkflows.map(({ workflow }) => workflow.id);
}

/**
* Check if user owns more than 15 workflows or more than 2 workflows with at least 2 nodes.
* If user does, set flag in its settings.
*/
export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {
let belowThreshold = true;
const skippedTypes = ['n8n-nodes-base.start', 'n8n-nodes-base.stickyNote'];

const workflowOwnerRole = await Db.collections.Role.findOne({
name: 'owner',
scope: 'workflow',
});
const ownedWorkflowsIds = await Db.collections.SharedWorkflow.find({
user,
role: workflowOwnerRole,
}).then((ownedWorkflows) => ownedWorkflows.map((wf) => wf.workflowId));

if (ownedWorkflowsIds.length > 15) {
belowThreshold = false;
} else {
// just fetch workflows' nodes to keep memory footprint low
const workflows = await Db.collections.Workflow.find({
where: { id: In(ownedWorkflowsIds) },
select: ['nodes'],
});

// valid workflow: 2+ nodes without start node
const validWorkflowCount = workflows.reduce((counter, workflow) => {
if (counter <= 2 && workflow.nodes.length > 2) {
const nodes = workflow.nodes.filter((node) => !skippedTypes.includes(node.type));
if (nodes.length >= 2) {
return counter + 1;
}
}
return counter;
}, 0);

// more than 2 valid workflows required
belowThreshold = validWorkflowCount <= 2;
}

// user is above threshold --> set flag in settings
if (!belowThreshold) {
void Db.collections.User.update(user.id, { settings: { isOnboarded: true } });
}

return belowThreshold;
}
10 changes: 6 additions & 4 deletions packages/cli/src/api/credentials.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,12 @@ credentialsController.get(
ResponseHelper.send(async (req: CredentialRequest.NewName): Promise<{ name: string }> => {
const { name: newName } = req.query;

return GenericHelpers.generateUniqueName(
newName ?? config.getEnv('credentials.defaultName'),
'credentials',
);
return {
name: await GenericHelpers.generateUniqueName(
newName ?? config.getEnv('credentials.defaultName'),
'credentials',
),
};
}),
);

Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/databases/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from 'typeorm';
import { IsEmail, IsString, Length } from 'class-validator';
import * as config from '../../../config';
import { DatabaseType, IPersonalizationSurveyAnswers } from '../..';
import { DatabaseType, IPersonalizationSurveyAnswers, IUserSettings } from '../..';
import { Role } from './Role';
import { SharedWorkflow } from './SharedWorkflow';
import { SharedCredentials } from './SharedCredentials';
Expand Down Expand Up @@ -102,6 +102,12 @@ export class User {
})
personalizationAnswers: IPersonalizationSurveyAnswers | null;

@Column({
type: resolveDataType('json') as ColumnOptions['type'],
nullable: true,
})
settings: IUserSettings | null;

@ManyToOne(() => Role, (role) => role.globalForUsers, {
cascade: true,
nullable: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';

export class AddUserSettings1652367743993 implements MigrationInterface {
name = 'AddUserSettings1652367743993';

public async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query(
'ALTER TABLE `' + tablePrefix + 'user` ADD COLUMN `settings` json NULL DEFAULT NULL',
);
await queryRunner.query(
'ALTER TABLE `' +
tablePrefix +
'user` CHANGE COLUMN `personalizationAnswers` `personalizationAnswers` json NULL DEFAULT NULL',
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query('ALTER TABLE `' + tablePrefix + 'user` DROP COLUMN `settings`');
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/mysqldb/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWo
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';

export const mysqlMigrations = [
InitialMigration1588157391238,
Expand All @@ -30,4 +31,5 @@ export const mysqlMigrations = [
AddExecutionEntityIndexes1644424784709,
CreateUserManagement1646992772331,
LowerCaseUserEmail1648740597343,
AddUserSettings1652367743993,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';

export class AddUserSettings1652367743993 implements MigrationInterface {
name = 'AddUserSettings1652367743993';

public async up(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.getEnv('database.tablePrefix');
const schema = config.getEnv('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}

await queryRunner.query(`ALTER TABLE ${tablePrefix}user ADD COLUMN settings json`);

await queryRunner.query(
`ALTER TABLE ${tablePrefix}user ALTER COLUMN "personalizationAnswers" TYPE json USING to_jsonb("personalizationAnswers")::json;`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
let tablePrefix = config.getEnv('database.tablePrefix');
const schema = config.getEnv('database.postgresdb.schema');
if (schema) {
tablePrefix = schema + '.' + tablePrefix;
}

await queryRunner.query(`ALTER TABLE ${tablePrefix}user DROP COLUMN settings`);
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/postgresdb/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { AddExecutionEntityIndexes1644422880309 } from './1644422880309-AddExecu
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';

export const postgresMigrations = [
InitialMigration1587669153312,
Expand All @@ -26,4 +27,5 @@ export const postgresMigrations = [
IncreaseTypeVarcharLimit1646834195327,
CreateUserManagement1646992772331,
LowerCaseUserEmail1648740597343,
AddUserSettings1652367743993,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';

export class AddUserSettings1652367743993 implements MigrationInterface {
name = 'AddUserSettings1652367743993';

public async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);

const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query('PRAGMA foreign_keys=OFF');

await queryRunner.query(
`CREATE TABLE "temporary_user" ("id" varchar PRIMARY KEY NOT NULL, "email" varchar(255), "firstName" varchar(32), "lastName" varchar(32), "password" varchar, "resetPasswordToken" varchar, "resetPasswordTokenExpiration" integer DEFAULT NULL, "personalizationAnswers" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "globalRoleId" integer NOT NULL, "settings" text, CONSTRAINT "FK_${tablePrefix}f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "${tablePrefix}role" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`,
);
await queryRunner.query(
`INSERT INTO "temporary_user"("id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId") SELECT "id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId" FROM "${tablePrefix}user"`,
);
await queryRunner.query(`DROP TABLE "${tablePrefix}user"`);
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "${tablePrefix}user"`);

await queryRunner.query(
`CREATE UNIQUE INDEX "UQ_${tablePrefix}e12875dfb3b1d92d7d7c5377e2" ON "${tablePrefix}user" ("email")`,
);

await queryRunner.query('PRAGMA foreign_keys=ON');

logMigrationEnd(this.name);
}

public async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.getEnv('database.tablePrefix');

await queryRunner.query('PRAGMA foreign_keys=OFF');

await queryRunner.query(`ALTER TABLE "${tablePrefix}user" RENAME TO "temporary_user"`);
await queryRunner.query(
`CREATE TABLE "${tablePrefix}user" ("id" varchar PRIMARY KEY NOT NULL, "email" varchar(255), "firstName" varchar(32), "lastName" varchar(32), "password" varchar, "resetPasswordToken" varchar, "resetPasswordTokenExpiration" integer DEFAULT NULL, "personalizationAnswers" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "globalRoleId" integer NOT NULL, CONSTRAINT "FK_${tablePrefix}f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "${tablePrefix}role" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`,
);
await queryRunner.query(
`INSERT INTO "${tablePrefix}user"("id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId") SELECT "id", "email", "firstName", "lastName", "password", "resetPasswordToken", "resetPasswordTokenExpiration", "personalizationAnswers", "createdAt", "updatedAt", "globalRoleId" FROM "temporary_user"`,
);
await queryRunner.query(`DROP TABLE "temporary_user"`);
await queryRunner.query(
`CREATE UNIQUE INDEX "UQ_${tablePrefix}e12875dfb3b1d92d7d7c5377e2" ON "${tablePrefix}user" ("email")`,
);

await queryRunner.query('PRAGMA foreign_keys=ON');
}
}
2 changes: 2 additions & 0 deletions packages/cli/src/databases/sqlite/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWo
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
import { AddUserSettings1652367743993 } from './1652367743993-AddUserSettings';

const sqliteMigrations = [
InitialMigration1588102412422,
Expand All @@ -26,6 +27,7 @@ const sqliteMigrations = [
AddExecutionEntityIndexes1644421939510,
CreateUserManagement1646992772331,
LowerCaseUserEmail1648740597343,
AddUserSettings1652367743993,
];

export { sqliteMigrations };
2 changes: 1 addition & 1 deletion packages/cli/src/requests.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export declare namespace WorkflowRequest {

type Update = AuthenticatedRequest<{ id: string }, {}, RequestBody>;

type NewName = express.Request<{}, {}, {}, { name?: string }>;
type NewName = AuthenticatedRequest<{}, {}, {}, { name?: string }>;

type GetAll = AuthenticatedRequest<{}, {}, {}, { filter: string }>;

Expand Down
27 changes: 26 additions & 1 deletion packages/design-system/src/components/N8nMarkdown/Markdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
v-if="!loading"
ref="editor"
:class="$style[theme]" v-html="htmlContent"
@click="onClick"
/>
<div v-else :class="$style.markdown">
<div v-for="(block, index) in loadingBlocks"
Expand Down Expand Up @@ -117,6 +118,7 @@ export default {
}

const fileIdRegex = new RegExp('fileId:([0-9]+)');
const imageFilesRegex = /\.(jpeg|jpg|gif|png|webp|bmp|tif|tiff|apng|svg|avif)$/;
let contentToRender = this.content;
if (this.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
Expand All @@ -129,7 +131,10 @@ export default {
const id = value.split('fileId:')[1];
return `src=${xss.friendlyAttrValue(imageUrls[id])}` || '';
}
if (!value.startsWith('https://')) {
// Only allow http requests to supported image files from the `static` directory
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
const isStaticImageFile = isImageFile && value.startsWith('/static/');
if (!value.startsWith('https://') && !isStaticImageFile) {
return '';
}
}
Expand All @@ -154,6 +159,22 @@ export default {
.use(markdownTasklists, this.options.tasklists),
};
},
methods: {
onClick(event) {
let clickedLink = null;

if(event.target instanceof HTMLAnchorElement) {
clickedLink = event.target;
}
if(event.target.matches('a *')) {
const parentLink = event.target.closest('a');
if(parentLink) {
clickedLink = parentLink;
}
}
this.$emit('markdown-click', clickedLink, event);
}
}
};
</script>

Expand Down Expand Up @@ -287,6 +308,10 @@ export default {

img {
object-fit: contain;

&[src*="#full-width"] {
width: 100%;
}
}
}

Expand Down
Loading