Compare commits
28 Commits
migrate-fl
...
v0.9.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
adf763c1b0 | ||
![]() |
36ee0df256 | ||
![]() |
823d85b24a | ||
![]() |
a3b3038709 | ||
![]() |
ddeb18f626 | ||
![]() |
90cd11bd38 | ||
![]() |
e9ba37b8de | ||
![]() |
d5e4a1b1ad | ||
![]() |
129e6d60e5 | ||
![]() |
4b77f2f590 | ||
![]() |
a909966562 | ||
![]() |
fd184239d6 | ||
![]() |
52bc49dc6a | ||
![]() |
b9352ccc06 | ||
![]() |
525b2baf06 | ||
![]() |
a8edeb2459 | ||
![]() |
e3830d64e0 | ||
![]() |
91f3e2c2b4 | ||
![]() |
77b4408416 | ||
![]() |
cede96f018 | ||
![]() |
8e0a28d238 | ||
![]() |
da5d594428 | ||
![]() |
9f9ee0bb58 | ||
![]() |
163aca6179 | ||
![]() |
cb06d3b0ae | ||
![]() |
dbe18dd100 | ||
![]() |
217970667a | ||
![]() |
dace794167 |
31
.github/workflows/playwright.yml
vendored
Normal file
31
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Automatisch UI Tests
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
working-directory: ./packages/e2e-tests
|
||||
env:
|
||||
LOGIN_EMAIL: ${{ secrets.LOGIN_EMAIL }}
|
||||
LOGIN_PASSWORD: ${{ secrets.LOGIN_PASSWORD }}
|
||||
run: yarn test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test-results/
|
||||
retention-days: 30
|
@@ -4,7 +4,7 @@ WORKDIR /automatisch
|
||||
|
||||
RUN \
|
||||
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
||||
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \
|
||||
yarn global add @automatisch/cli@0.9.0 --network-timeout 1000000 && \
|
||||
rm -rf /usr/local/share/.cache/ && \
|
||||
apk del build-dependencies
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM automatischio/automatisch:0.8.0
|
||||
FROM automatischio/automatisch:0.9.0
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apk add --no-cache openssl dos2unix
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
@@ -22,7 +22,7 @@
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.8.0",
|
||||
"@automatisch/web": "^0.9.0",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
@@ -110,7 +110,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@automatisch/types": "^0.9.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const role = await knex('roles')
|
||||
.select('id')
|
||||
.whereIn('key', ['user', 'admin'])
|
||||
.orderBy('key', 'desc')
|
||||
.limit(1)
|
||||
.first();
|
||||
|
||||
if (role) {
|
||||
// backfill nulls
|
||||
await knex('users').whereNull('role_id').update({ role_id: role.id });
|
||||
}
|
||||
|
||||
return await knex.schema.alterTable('users', (table) => {
|
||||
table.uuid('role_id').notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return await knex.schema.alterTable('users', (table) => {
|
||||
table.uuid('role_id').nullable().alter();
|
||||
});
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const users = await knex('users').whereNotNull('deleted_at');
|
||||
const userIds = users.map((user) => user.id);
|
||||
|
||||
const flows = await knex('flows').whereIn('user_id', userIds);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
const executions = await knex('executions').whereIn('flow_id', flowIds);
|
||||
const executionIds = executions.map((execution) => execution.id);
|
||||
|
||||
await knex('execution_steps').whereIn('execution_id', executionIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('executions').whereIn('id', executionIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('steps').whereIn('flow_id', flowIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('flows').whereIn('id', flowIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
await knex('connections').whereIn('user_id', userIds).update({
|
||||
deleted_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// void
|
||||
}
|
@@ -1,17 +1,59 @@
|
||||
import { Duration } from 'luxon';
|
||||
import Context from '../../types/express/context';
|
||||
import deleteUserQueue from '../../queues/delete-user.ee';
|
||||
import flowQueue from '../../queues/flow';
|
||||
import Flow from '../../models/flow';
|
||||
import Execution from '../../models/execution';
|
||||
import ExecutionStep from '../../models/execution-step';
|
||||
import appConfig from '../../config/app';
|
||||
|
||||
const deleteCurrentUser = async (_parent: unknown, params: never, context: Context) => {
|
||||
const deleteCurrentUser = async (
|
||||
_parent: unknown,
|
||||
params: never,
|
||||
context: Context
|
||||
) => {
|
||||
const id = context.currentUser.id;
|
||||
|
||||
const flows = await context.currentUser.$relatedQuery('flows').where({
|
||||
active: true,
|
||||
});
|
||||
|
||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||
|
||||
for (const flow of flows) {
|
||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||
|
||||
if (job) {
|
||||
await flowQueue.removeRepeatableByKey(job.key);
|
||||
}
|
||||
}
|
||||
|
||||
const executionIds = (
|
||||
await context.currentUser
|
||||
.$relatedQuery('executions')
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
const flowIds = flows.map((flow) => flow.id);
|
||||
|
||||
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
||||
await context.currentUser.$relatedQuery('executions').delete();
|
||||
await context.currentUser.$relatedQuery('steps').delete();
|
||||
await Flow.query().whereIn('id', flowIds).delete();
|
||||
await context.currentUser.$relatedQuery('connections').delete();
|
||||
await context.currentUser.$relatedQuery('identities').delete();
|
||||
|
||||
if (appConfig.isCloud) {
|
||||
await context.currentUser.$relatedQuery('subscriptions').delete();
|
||||
await context.currentUser.$relatedQuery('usageData').delete();
|
||||
}
|
||||
|
||||
await context.currentUser.$query().delete();
|
||||
|
||||
const jobName = `Delete user - ${id}`;
|
||||
const jobPayload = { id };
|
||||
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
|
||||
const jobOptions = {
|
||||
delay: millisecondsFor30Days
|
||||
delay: millisecondsFor30Days,
|
||||
};
|
||||
|
||||
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import Context from '../../types/express/context';
|
||||
import testRun from '../../services/test-run';
|
||||
import Step from '../../models/step';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -12,12 +13,16 @@ const executeFlow = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
const conditions = context.currentUser.can('update', 'Flow');
|
||||
const isCreator = conditions.isCreator;
|
||||
const allSteps = Step.query();
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const baseQuery = isCreator ? userSteps : allSteps;
|
||||
|
||||
const { stepId } = params.input;
|
||||
|
||||
const untilStep = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
const untilStep = await baseQuery
|
||||
.clone()
|
||||
.findById(stepId)
|
||||
.throwIfNotFound();
|
||||
|
||||
|
@@ -8,7 +8,11 @@ type Params = {
|
||||
};
|
||||
};
|
||||
|
||||
const updateConfig = async (_parent: unknown, params: Params, context: Context) => {
|
||||
const updateConfig = async (
|
||||
_parent: unknown,
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Config');
|
||||
|
||||
const config = params.input;
|
||||
@@ -18,22 +22,26 @@ const updateConfig = async (_parent: unknown, params: Params, context: Context)
|
||||
for (const key of configKeys) {
|
||||
const newValue = config[key];
|
||||
|
||||
const entryUpdate = Config
|
||||
.query()
|
||||
.insert({
|
||||
key,
|
||||
value: {
|
||||
data: newValue
|
||||
}
|
||||
})
|
||||
.onConflict('key')
|
||||
.merge({
|
||||
value: {
|
||||
data: newValue
|
||||
}
|
||||
});
|
||||
if (newValue) {
|
||||
const entryUpdate = Config.query()
|
||||
.insert({
|
||||
key,
|
||||
value: {
|
||||
data: newValue,
|
||||
},
|
||||
})
|
||||
.onConflict('key')
|
||||
.merge({
|
||||
value: {
|
||||
data: newValue,
|
||||
},
|
||||
});
|
||||
|
||||
updates.push(entryUpdate);
|
||||
updates.push(entryUpdate);
|
||||
} else {
|
||||
const entryUpdate = Config.query().findOne({ key }).delete();
|
||||
updates.push(entryUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(updates);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import Flow from '../../models/flow';
|
||||
import Context from '../../types/express/context';
|
||||
import flowQueue from '../../queues/flow';
|
||||
import { REMOVE_AFTER_30_DAYS_OR_150_JOBS, REMOVE_AFTER_7_DAYS_OR_50_JOBS } from '../../helpers/remove-job-configuration';
|
||||
@@ -18,10 +19,14 @@ const updateFlowStatus = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('publish', 'Flow');
|
||||
const conditions = context.currentUser.can('publish', 'Flow');
|
||||
const isCreator = conditions.isCreator;
|
||||
const allFlows = Flow.query();
|
||||
const userFlows = context.currentUser.$relatedQuery('flows');
|
||||
const baseQuery = isCreator ? userFlows : allFlows;
|
||||
|
||||
let flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
let flow = await baseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import App from '../../models/app';
|
||||
import Step from '../../models/step';
|
||||
import Connection from '../../models/connection';
|
||||
import Context from '../../types/express/context';
|
||||
|
||||
type Params = {
|
||||
@@ -23,12 +24,14 @@ const updateStep = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
const { isCreator } = context.currentUser.can('update', 'Flow');
|
||||
const userSteps = context.currentUser.$relatedQuery('steps');
|
||||
const allSteps = Step.query();
|
||||
const baseQuery = isCreator ? userSteps : allSteps;
|
||||
|
||||
const { input } = params;
|
||||
|
||||
let step = await context.currentUser
|
||||
.$relatedQuery('steps')
|
||||
let step = await baseQuery
|
||||
.findOne({
|
||||
'steps.id': input.id,
|
||||
flow_id: input.flow.id,
|
||||
@@ -36,11 +39,24 @@ const updateStep = async (
|
||||
.throwIfNotFound();
|
||||
|
||||
if (input.connection.id) {
|
||||
const hasConnection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
.findById(input.connection?.id);
|
||||
let canSeeAllConnections = false;
|
||||
try {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
if (!hasConnection) {
|
||||
canSeeAllConnections = !conditions.isCreator;
|
||||
} catch {
|
||||
// void
|
||||
}
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
||||
|
||||
const connection = await baseConnectionsQuery
|
||||
.clone()
|
||||
.findById(input.connection?.id)
|
||||
|
||||
if (!connection) {
|
||||
throw new Error('The connection does not exist!');
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import Context from '../../types/express/context';
|
||||
import SamlAuthProvider from '../../models/saml-auth-provider.ee';
|
||||
|
||||
type Params = {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const getSamlAuthProviderRoleMappings = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'SamlAuthProvider');
|
||||
|
||||
const samlAuthProvider = await SamlAuthProvider
|
||||
.query()
|
||||
.findById(params.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
const roleMappings = await samlAuthProvider
|
||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||
.orderBy('remote_role_name', 'asc')
|
||||
|
||||
return roleMappings;
|
||||
};
|
||||
|
||||
export default getSamlAuthProviderRoleMappings;
|
@@ -10,15 +10,14 @@ type Params = {
|
||||
const getUsers = async (_parent: unknown, params: Params, context: Context) => {
|
||||
context.currentUser.can('read', 'User');
|
||||
|
||||
const usersQuery = User
|
||||
.query()
|
||||
const usersQuery = User.query()
|
||||
.leftJoinRelated({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
role: true
|
||||
role: true,
|
||||
})
|
||||
.orderBy('full_name', 'desc');
|
||||
.orderBy('full_name', 'asc');
|
||||
|
||||
return paginate(usersQuery, params.limit, params.offset);
|
||||
};
|
||||
|
@@ -22,6 +22,7 @@ import getPermissionCatalog from './queries/get-permission-catalog.ee';
|
||||
import getRole from './queries/get-role.ee';
|
||||
import getRoles from './queries/get-roles.ee';
|
||||
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
|
||||
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
|
||||
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
|
||||
import getSubscriptionStatus from './queries/get-subscription-status.ee';
|
||||
import getTrialStatus from './queries/get-trial-status.ee';
|
||||
@@ -56,6 +57,7 @@ const queryResolvers = {
|
||||
getRole,
|
||||
getRoles,
|
||||
getSamlAuthProvider,
|
||||
getSamlAuthProviderRoleMappings,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
|
@@ -47,6 +47,7 @@ type Query {
|
||||
getRole(id: String!): Role
|
||||
getRoles: [Role]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getTrialStatus: GetTrialStatus
|
||||
getUser(id: String!): User
|
||||
@@ -329,6 +330,7 @@ type SamlAuthProvider {
|
||||
emailAttributeName: String
|
||||
roleAttributeName: String
|
||||
active: Boolean
|
||||
defaultRoleId: String
|
||||
}
|
||||
|
||||
type SamlAuthProvidersRoleMapping {
|
||||
@@ -341,6 +343,7 @@ type SamlAuthProvidersRoleMapping {
|
||||
type UserConnection {
|
||||
edges: [UserEdge]
|
||||
pageInfo: PageInfo
|
||||
totalCount: Int
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
|
@@ -2,8 +2,7 @@ import Step from '../models/step';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
import get from 'lodash.get';
|
||||
|
||||
// INFO: don't remove space in allowed character group!
|
||||
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ :]+)+}})/g;
|
||||
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[^.}{]+)+}})/g;
|
||||
|
||||
export default function computeParameters(
|
||||
parameters: Step['parameters'],
|
||||
|
@@ -48,7 +48,7 @@ const findOrCreateUserBySamlIdentity = async (
|
||||
.join(' '),
|
||||
email: mappedUser.email as string,
|
||||
roleId:
|
||||
samlAuthProviderRoleMapping.roleId || samlAuthProvider.defaultRoleId,
|
||||
samlAuthProviderRoleMapping?.roleId || samlAuthProvider.defaultRoleId,
|
||||
identities: [
|
||||
{
|
||||
remoteId: mappedUser.id as string,
|
||||
|
@@ -21,6 +21,7 @@ const paginate = async (
|
||||
currentPage: Math.ceil(offset / limit + 1),
|
||||
totalPages: Math.ceil(count / limit),
|
||||
},
|
||||
totalCount: count,
|
||||
edges: records.map((record: Base) => ({
|
||||
node: record,
|
||||
})),
|
||||
|
@@ -3,6 +3,7 @@ import { Worker } from 'bullmq';
|
||||
import * as Sentry from '../helpers/sentry.ee';
|
||||
import redisConfig from '../config/redis';
|
||||
import logger from '../helpers/logger';
|
||||
import appConfig from '../config/app';
|
||||
import User from '../models/user';
|
||||
import Execution from '../models/execution';
|
||||
import ExecutionStep from '../models/execution-step';
|
||||
@@ -12,21 +13,34 @@ export const worker = new Worker(
|
||||
async (job) => {
|
||||
const { id } = job.data;
|
||||
|
||||
const user = await User.query().findById(id).throwIfNotFound();
|
||||
const user = await User.query()
|
||||
.withSoftDeleted()
|
||||
.findById(id)
|
||||
.throwIfNotFound();
|
||||
|
||||
const executionIds = (
|
||||
await user.$relatedQuery('executions').select('executions.id')
|
||||
await user
|
||||
.$relatedQuery('executions')
|
||||
.withSoftDeleted()
|
||||
.select('executions.id')
|
||||
).map((execution: Execution) => execution.id);
|
||||
|
||||
await ExecutionStep.query()
|
||||
.hardDelete()
|
||||
.whereIn('execution_id', executionIds);
|
||||
await user.$relatedQuery('executions').hardDelete();
|
||||
await user.$relatedQuery('steps').hardDelete();
|
||||
await user.$relatedQuery('flows').hardDelete();
|
||||
await user.$relatedQuery('connections').hardDelete();
|
||||
.withSoftDeleted()
|
||||
.whereIn('execution_id', executionIds)
|
||||
.hardDelete();
|
||||
await user.$relatedQuery('executions').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('steps').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('flows').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('connections').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('identities').withSoftDeleted().hardDelete();
|
||||
|
||||
await user.$query().hardDelete();
|
||||
if (appConfig.isCloud) {
|
||||
await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete();
|
||||
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
|
||||
}
|
||||
|
||||
await user.$query().withSoftDeleted().hardDelete();
|
||||
},
|
||||
{ connection: redisConfig }
|
||||
);
|
||||
|
@@ -3,6 +3,7 @@ import { Worker } from 'bullmq';
|
||||
import * as Sentry from '../helpers/sentry.ee';
|
||||
import redisConfig from '../config/redis';
|
||||
import logger from '../helpers/logger';
|
||||
import flowQueue from '../queues/flow';
|
||||
import triggerQueue from '../queues/trigger';
|
||||
import { processFlow } from '../services/flow';
|
||||
import Flow from '../models/flow';
|
||||
@@ -66,7 +67,7 @@ worker.on('completed', (job) => {
|
||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
worker.on('failed', async (job, err) => {
|
||||
const errorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||
\n ${err.stack}
|
||||
@@ -74,6 +75,18 @@ worker.on('failed', (job, err) => {
|
||||
|
||||
logger.error(errorMessage);
|
||||
|
||||
const flow = await Flow.query().findById(job.data.flowId);
|
||||
|
||||
if (!flow) {
|
||||
await flowQueue.removeRepeatableByKey(job.repeatJobKey);
|
||||
|
||||
const flowNotFoundErrorMessage = `
|
||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has been deleted from Redis because flow was not found!
|
||||
`;
|
||||
|
||||
logger.error(flowNotFoundErrorMessage);
|
||||
}
|
||||
|
||||
Sentry.captureException(err, {
|
||||
extra: {
|
||||
jobId: job.id,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/cli",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"contributors": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/backend": "^0.8.0",
|
||||
"@automatisch/backend": "^0.9.0",
|
||||
"@oclif/core": "^1",
|
||||
"@oclif/plugin-help": "^5",
|
||||
"@oclif/plugin-plugins": "^2.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/docs",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"private": true,
|
||||
|
5
packages/e2e-tests/.gitignore
vendored
Normal file
5
packages/e2e-tests/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
/output
|
@@ -1,17 +0,0 @@
|
||||
const { defineConfig } = require('cypress');
|
||||
|
||||
const TO_BE_PROVIDED = 'HAS_TO_BE_PROVIDED_IN_cypress.env.json';
|
||||
|
||||
module.exports = defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3001',
|
||||
env: {
|
||||
login_email: 'user@automatisch.io',
|
||||
login_password: 'sample',
|
||||
deepl_auth_key: TO_BE_PROVIDED,
|
||||
},
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 768,
|
||||
},
|
||||
});
|
@@ -1,52 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Apps page', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
||||
cy.og('apps-page-drawer-link').click();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.logout();
|
||||
});
|
||||
|
||||
it('displays applications', () => {
|
||||
cy.og('apps-loader').should('not.exist');
|
||||
cy.og('app-row').should('have.length.least', 1);
|
||||
|
||||
cy.ss('Applications');
|
||||
});
|
||||
|
||||
context('can add connection', () => {
|
||||
before(() => {
|
||||
cy
|
||||
.og('add-connection-button')
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
it('lists applications', () => {
|
||||
cy.og('app-list-item').should('have.length.above', 1);
|
||||
});
|
||||
|
||||
it('searches an application', () => {
|
||||
cy.og('search-for-app-text-field').type('DeepL');
|
||||
cy.og('app-list-item').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('goes to app page to create a connection', () => {
|
||||
cy.og('app-list-item').first().click();
|
||||
|
||||
cy.location('pathname').should('equal', '/app/deepl/connections/add');
|
||||
|
||||
cy.og('add-app-connection-dialog').should('be.visible');
|
||||
});
|
||||
|
||||
it('closes the dialog on backdrop click', () => {
|
||||
cy.clickOutside();
|
||||
|
||||
cy.location('pathname').should('equal', '/app/deepl/connections');
|
||||
cy.og('add-app-connection-dialog').should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,48 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Connections page', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
||||
cy.og('apps-page-drawer-link').click();
|
||||
|
||||
cy.visit('/app/deepl/connections');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.logout();
|
||||
});
|
||||
|
||||
it('shows connections if any', () => {
|
||||
cy.og('apps-loader').should('not.exist');
|
||||
|
||||
cy.ss('DeepL connections before creating a connection');
|
||||
});
|
||||
|
||||
context('can add connection', () => {
|
||||
it('has a button to open add connection dialog', () => {
|
||||
cy.scrollTo('top', { ensureScrollable: false });
|
||||
|
||||
cy
|
||||
.og('add-connection-button')
|
||||
.should('be.visible');
|
||||
});
|
||||
|
||||
it('add connection button takes user to add connection page', () => {
|
||||
cy.og('add-connection-button').click();
|
||||
|
||||
cy.location('pathname').should('equal', '/app/deepl/connections/add');
|
||||
});
|
||||
|
||||
it('shows add connection dialog to create a new connection', () => {
|
||||
cy.get('input[name="screenName"]').type('e2e-test connection!');
|
||||
cy.get('input[name="authenticationKey"]').type(Cypress.env('deepl_auth_key'));
|
||||
|
||||
cy.og('create-connection-button').click();
|
||||
|
||||
cy.og('create-connection-button').should('not.exist');
|
||||
|
||||
cy.ss('DeepL connections after creating a connection');
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,32 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Execution page', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
||||
cy.og('executions-page-drawer-link').click();
|
||||
cy.og('execution-row').first().click({ force: true });
|
||||
|
||||
cy.location('pathname').should('match', /^\/executions\//);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.logout();
|
||||
});
|
||||
|
||||
it('displays data in by default', () => {
|
||||
cy.og('execution-step').should('have.length', 2);
|
||||
|
||||
cy.ss('Execution - data in');
|
||||
});
|
||||
|
||||
it('displays data out', () => {
|
||||
cy.og('data-out-tab').click({ multiple: true });
|
||||
|
||||
cy.ss('Execution - data out');
|
||||
});
|
||||
|
||||
it('does not display error', () => {
|
||||
cy.og('error-tab').should('not.exist');
|
||||
});
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Executions page', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
||||
cy.og('executions-page-drawer-link').click();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.logout();
|
||||
});
|
||||
|
||||
it('displays executions', () => {
|
||||
cy.og('executions-loader').should('not.exist');
|
||||
cy.og('execution-row').should('exist');
|
||||
|
||||
cy.ss('Executions');
|
||||
});
|
||||
});
|
@@ -1,217 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Flow editor page', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.logout();
|
||||
});
|
||||
|
||||
it('create flow', () => {
|
||||
cy.og('create-flow-button').click({ force: true });
|
||||
});
|
||||
|
||||
it('has two steps by default', () => {
|
||||
cy.og('flow-step').should('have.length', 2);
|
||||
});
|
||||
|
||||
context('edit flow', () => {
|
||||
context('arrange Scheduler trigger', () => {
|
||||
context('choose app and event substep', () => {
|
||||
it('choose application', () => {
|
||||
cy.og('choose-app-autocomplete').click();
|
||||
|
||||
cy.get('li[role="option"]:contains("Scheduler")').click();
|
||||
});
|
||||
|
||||
it('choose an event', () => {
|
||||
cy.og('choose-event-autocomplete').should('be.visible').click();
|
||||
|
||||
cy.get('li[role="option"]:contains("Every hour")').click();
|
||||
});
|
||||
|
||||
it('continue to next step', () => {
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
|
||||
it('collapses the substep', () => {
|
||||
cy.og('choose-app-autocomplete').should('not.be.visible');
|
||||
cy.og('choose-event-autocomplete').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
context('set up a trigger', () => {
|
||||
it('choose "yes" in "trigger on weekends?"', () => {
|
||||
cy.og('parameters.triggersOnWeekend-autocomplete')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
cy.get('li[role="option"]:contains("Yes")').click();
|
||||
});
|
||||
|
||||
it('continue to next step', () => {
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
|
||||
it('collapses the substep', () => {
|
||||
cy.og('parameters.triggersOnWeekend-autocomplete').should(
|
||||
'not.exist'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
context('test trigger', () => {
|
||||
it('show sample output', () => {
|
||||
cy.og('flow-test-substep-output').should('not.exist');
|
||||
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
|
||||
cy.og('flow-test-substep-output').should('be.visible');
|
||||
|
||||
cy.ss('Scheduler trigger test output');
|
||||
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('arrange DeepL action', () => {
|
||||
context('choose app and event substep', () => {
|
||||
it('choose application', () => {
|
||||
cy.og('choose-app-autocomplete').click();
|
||||
|
||||
cy.get('li[role="option"]:contains("DeepL")').click();
|
||||
});
|
||||
|
||||
it('choose an event', () => {
|
||||
cy.og('choose-event-autocomplete').should('be.visible').click();
|
||||
|
||||
cy.get(
|
||||
'li[role="option"]:contains("Translate Text")'
|
||||
).click();
|
||||
});
|
||||
|
||||
it('continue to next step', () => {
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
|
||||
it('collapses the substep', () => {
|
||||
cy.og('choose-app-autocomplete').should('not.be.visible');
|
||||
cy.og('choose-event-autocomplete').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
context('choose connection', () => {
|
||||
it('choose connection', () => {
|
||||
cy.og('choose-connection-autocomplete').click();
|
||||
|
||||
cy.get('li[role="option"]').first().click();
|
||||
});
|
||||
|
||||
it('continue to next step', () => {
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
|
||||
it('collapses the substep', () => {
|
||||
cy.og('choose-connection-autocomplete').should('not.be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
context('set up action', () => {
|
||||
it('arrange text', () => {
|
||||
cy.og('power-input', ' [contenteditable]')
|
||||
.click()
|
||||
.type(
|
||||
`Hello from e2e tests! Here is the first suggested variable's value; `
|
||||
);
|
||||
|
||||
cy.og('power-input-suggestion-group')
|
||||
.first()
|
||||
.og('power-input-suggestion-item')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.clickOutside();
|
||||
|
||||
cy.ss('DeepL action text');
|
||||
});
|
||||
|
||||
it('set target language', () => {
|
||||
cy.og('parameters.targetLanguage-autocomplete').click();
|
||||
|
||||
cy.get(
|
||||
'li[role="option"]:contains("Turkish")'
|
||||
).first().click();
|
||||
});
|
||||
|
||||
it('continue to next step', () => {
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
|
||||
it('collapses the substep', () => {
|
||||
cy.og('power-input', ' [contenteditable]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
context('test trigger', () => {
|
||||
it('show sample output', () => {
|
||||
cy.og('flow-test-substep-output').should('not.exist');
|
||||
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
|
||||
cy.og('flow-test-substep-output').should('be.visible');
|
||||
|
||||
cy.ss('DeepL action test output');
|
||||
|
||||
cy.og('flow-substep-continue-button').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('publish and unpublish', () => {
|
||||
it('publish flow', () => {
|
||||
cy.og('unpublish-flow-button').should('not.exist');
|
||||
|
||||
cy.og('publish-flow-button').should('be.visible').click();
|
||||
|
||||
cy.og('publish-flow-button').should('not.exist');
|
||||
});
|
||||
|
||||
it('shows read-only sticky snackbar', () => {
|
||||
cy.og('flow-cannot-edit-info-snackbar').should('be.visible');
|
||||
|
||||
cy.ss('Published flow');
|
||||
});
|
||||
|
||||
it('unpublish from snackbar', () => {
|
||||
cy.og('unpublish-flow-from-snackbar').click();
|
||||
|
||||
cy.og('flow-cannot-edit-info-snackbar').should('not.exist');
|
||||
});
|
||||
|
||||
it('publish once again', () => {
|
||||
cy.og('publish-flow-button').should('be.visible').click();
|
||||
|
||||
cy.og('publish-flow-button').should('not.exist');
|
||||
});
|
||||
|
||||
it('unpublish from layout top bar', () => {
|
||||
cy.og('unpublish-flow-button').should('be.visible').click();
|
||||
|
||||
cy.og('unpublish-flow-button').should('not.exist');
|
||||
|
||||
cy.ss('Unpublished flow');
|
||||
});
|
||||
});
|
||||
|
||||
context('in layout', () => {
|
||||
it('can go back to flows page', () => {
|
||||
cy.og('editor-go-back-button').click();
|
||||
|
||||
cy.location('pathname').should('equal', '/flows');
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,44 +0,0 @@
|
||||
Cypress.Commands.add(
|
||||
'og',
|
||||
{ prevSubject: 'optional' },
|
||||
(subject, selector, suffix = '') => {
|
||||
if (subject) {
|
||||
return cy.wrap(subject).get(`[data-test="${selector}"]${suffix}`);
|
||||
}
|
||||
|
||||
return cy.get(`[data-test="${selector}"]${suffix}`);
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('login', () => {
|
||||
cy.visit('/login');
|
||||
|
||||
cy.og('email-text-field').type(Cypress.env('login_email'));
|
||||
cy.og('password-text-field').type(Cypress.env('login_password'));
|
||||
|
||||
cy.intercept('/graphql').as('graphqlCalls');
|
||||
cy.intercept('https://notifications.automatisch.io/notifications.json').as(
|
||||
'notificationsCall'
|
||||
);
|
||||
cy.og('login-button').click();
|
||||
|
||||
cy.wait(['@graphqlCalls', '@notificationsCall']);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('logout', () => {
|
||||
cy.og('profile-menu-button').click();
|
||||
|
||||
cy.og('logout-item').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('ss', (name, opts = {}) => {
|
||||
return cy.screenshot(name, {
|
||||
overwrite: true,
|
||||
capture: 'viewport',
|
||||
...opts,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clickOutside', () => {
|
||||
return cy.get('body').click(0, 0);
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
16
packages/e2e-tests/fixtures/applications-page.js
Normal file
16
packages/e2e-tests/fixtures/applications-page.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ApplicationsPage extends AuthenticatedPage {
|
||||
screenshotPath = '/applications';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.drawerLink = this.page.getByTestId('apps-page-drawer-link');
|
||||
this.addConnectionButton = this.page.getByTestId('add-connection-button');
|
||||
}
|
||||
}
|
21
packages/e2e-tests/fixtures/authenticated-page.js
Normal file
21
packages/e2e-tests/fixtures/authenticated-page.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const path = require('node:path');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { BasePage } = require('./base-page');
|
||||
const { LoginPage } = require('./login-page');
|
||||
|
||||
export class AuthenticatedPage extends BasePage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
|
||||
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
|
||||
this.userInterfaceDrawerItem = this.page.getByTestId('user-interface-drawer-link');
|
||||
this.appBar = this.page.getByTestId('app-bar');
|
||||
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
|
||||
this.typographyLogo = this.page.getByTestId('typography-logo');
|
||||
this.customLogo = this.page.getByTestId('custom-logo');
|
||||
}
|
||||
}
|
29
packages/e2e-tests/fixtures/base-page.js
Normal file
29
packages/e2e-tests/fixtures/base-page.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require('node:path');
|
||||
|
||||
export class BasePage {
|
||||
screenshotPath = '/';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.snackbar = this.page.locator('#notistack-snackbar');
|
||||
}
|
||||
|
||||
async clickAway() {
|
||||
await this.page.locator('body').click({ position: { x: 0, y: 0 } });
|
||||
}
|
||||
|
||||
async screenshot(options = {}) {
|
||||
const { path: plainPath, ...restOptions } = options;
|
||||
|
||||
const computedPath = path.join(
|
||||
'output/screenshots',
|
||||
this.screenshotPath,
|
||||
plainPath
|
||||
);
|
||||
|
||||
return await this.page.screenshot({ path: computedPath, ...restOptions });
|
||||
}
|
||||
}
|
10
packages/e2e-tests/fixtures/connections-page.js
Normal file
10
packages/e2e-tests/fixtures/connections-page.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ConnectionsPage extends AuthenticatedPage {
|
||||
screenshotPath = '/connections';
|
||||
|
||||
async clickAddConnectionButton() {
|
||||
await this.page.getByTestId('add-connection-button').click();
|
||||
}
|
||||
}
|
6
packages/e2e-tests/fixtures/executions-page.js
Normal file
6
packages/e2e-tests/fixtures/executions-page.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class ExecutionsPage extends AuthenticatedPage {
|
||||
screenshotPath = '/executions';
|
||||
}
|
26
packages/e2e-tests/fixtures/flow-editor-page.js
Normal file
26
packages/e2e-tests/fixtures/flow-editor-page.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class FlowEditorPage extends AuthenticatedPage {
|
||||
screenshotPath = '/flow-editor';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete');
|
||||
this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete');
|
||||
this.continueButton = this.page.getByTestId('flow-substep-continue-button');
|
||||
this.connectionAutocomplete = this.page.getByTestId(
|
||||
'choose-connection-autocomplete'
|
||||
);
|
||||
this.testOuput = this.page.getByTestId('flow-test-substep-output');
|
||||
this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button');
|
||||
this.publishFlowButton = this.page.getByTestId('publish-flow-button');
|
||||
this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar');
|
||||
this.trigger = this.page.getByLabel('Trigger on weekends?');
|
||||
this.stepCircularLoader = this.page.getByTestId('step-circular-loader');
|
||||
}
|
||||
}
|
40
packages/e2e-tests/fixtures/index.js
Normal file
40
packages/e2e-tests/fixtures/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { ApplicationsPage } = require('./applications-page');
|
||||
const { ConnectionsPage } = require('./connections-page');
|
||||
const { ExecutionsPage } = require('./executions-page');
|
||||
const { FlowEditorPage } = require('./flow-editor-page');
|
||||
const { UserInterfacePage } = require('./user-interface-page');
|
||||
const { LoginPage } = require('./login-page');
|
||||
|
||||
exports.test = test.extend({
|
||||
page: async ({ page }, use) => {
|
||||
await new LoginPage(page).login();
|
||||
|
||||
await use(page);
|
||||
},
|
||||
applicationsPage: async ({ page }, use) => {
|
||||
await use(new ApplicationsPage(page));
|
||||
},
|
||||
connectionsPage: async ({ page }, use) => {
|
||||
await use(new ConnectionsPage(page));
|
||||
},
|
||||
executionsPage: async ({ page }, use) => {
|
||||
await use(new ExecutionsPage(page));
|
||||
},
|
||||
flowEditorPage: async ({ page }, use) => {
|
||||
await use(new FlowEditorPage(page));
|
||||
},
|
||||
userInterfacePage: async ({ page }, use) => {
|
||||
await use(new UserInterfacePage(page));
|
||||
},
|
||||
});
|
||||
|
||||
expect.extend({
|
||||
toBeClickableLink: async (locator) => {
|
||||
await expect(locator).not.toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
return { pass: true };
|
||||
},
|
||||
});
|
||||
|
||||
exports.expect = expect;
|
29
packages/e2e-tests/fixtures/login-page.js
Normal file
29
packages/e2e-tests/fixtures/login-page.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require('node:path');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { BasePage } = require('./base-page');
|
||||
|
||||
export class LoginPage extends BasePage {
|
||||
path = '/login';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.page = page;
|
||||
this.emailTextField = this.page.getByTestId('email-text-field');
|
||||
this.passwordTextField = this.page.getByTestId('password-text-field');
|
||||
this.loginButton = this.page.getByTestId('login-button');
|
||||
}
|
||||
|
||||
async login() {
|
||||
await this.page.goto(this.path);
|
||||
await this.emailTextField.fill(process.env.LOGIN_EMAIL);
|
||||
await this.passwordTextField.fill(process.env.LOGIN_PASSWORD);
|
||||
|
||||
await this.loginButton.click();
|
||||
|
||||
await expect(this.loginButton).not.toBeVisible();
|
||||
}
|
||||
}
|
53
packages/e2e-tests/fixtures/user-interface-page.js
Normal file
53
packages/e2e-tests/fixtures/user-interface-page.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const path = require('node:path');
|
||||
const { AuthenticatedPage } = require('./authenticated-page');
|
||||
|
||||
export class UserInterfacePage extends AuthenticatedPage {
|
||||
screenshotPath = '/user-interface';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
this.flowRowCardActionArea = this.page
|
||||
.getByTestId('flow-row')
|
||||
.first()
|
||||
.getByTestId('card-action-area');
|
||||
this.updateButton = this.page.getByTestId('update-button');
|
||||
this.primaryMainColorInput = this.page
|
||||
.getByTestId('primary-main-color-input')
|
||||
.getByTestId('color-text-field');
|
||||
this.primaryDarkColorInput = this.page
|
||||
.getByTestId('primary-dark-color-input')
|
||||
.getByTestId('color-text-field');
|
||||
this.primaryLightColorInput = this.page
|
||||
.getByTestId('primary-light-color-input')
|
||||
.getByTestId('color-text-field');
|
||||
this.logoSvgCodeInput = this.page.getByTestId('logo-svg-data-text-field');
|
||||
this.primaryMainColorButton = this.page
|
||||
.getByTestId('primary-main-color-input')
|
||||
.getByTestId('color-button');
|
||||
this.primaryDarkColorButton = this.page
|
||||
.getByTestId('primary-dark-color-input')
|
||||
.getByTestId('color-button');
|
||||
this.primaryLightColorButton = this.page
|
||||
.getByTestId('primary-light-color-input')
|
||||
.getByTestId('color-button');
|
||||
}
|
||||
|
||||
hexToRgb(hexColor) {
|
||||
hexColor = hexColor.replace('#', '');
|
||||
const r = parseInt(hexColor.substring(0, 2), 16);
|
||||
const g = parseInt(hexColor.substring(2, 4), 16);
|
||||
const b = parseInt(hexColor.substring(4, 6), 16);
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
encodeSVG(svgCode) {
|
||||
const encoded = encodeURIComponent(svgCode);
|
||||
|
||||
return `data:image/svg+xml;utf8,${encoded}`;
|
||||
}
|
||||
}
|
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@automatisch/e2e-tests",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"private": true,
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
"open": "cypress open"
|
||||
"test": "playwright test",
|
||||
"test:fast": "yarn test -j 90% --quiet --reporter null --ignore-snapshots -x"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
@@ -22,6 +23,9 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^10.9.0"
|
||||
"@playwright/test": "^1.36.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1"
|
||||
}
|
||||
}
|
||||
|
89
packages/e2e-tests/playwright.config.js
Normal file
89
packages/e2e-tests/playwright.config.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// @ts-check
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
require('dotenv').config();
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Timeout threshold for each test */
|
||||
timeout: 120000,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: process.env.CI ? 'github' : 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.CI
|
||||
? 'https://sandbox.automatisch.io'
|
||||
: 'http://localhost:3001',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
testIdAttribute: 'data-test',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
},
|
||||
|
||||
expect: {
|
||||
/* Timeout threshold for each assertion */
|
||||
timeout: 30000,
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
73
packages/e2e-tests/tests/apps/list-apps.spec.js
Normal file
73
packages/e2e-tests/tests/apps/list-apps.spec.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Apps page', () => {
|
||||
test.beforeEach(async ({ applicationsPage }) => {
|
||||
await applicationsPage.drawerLink.click();
|
||||
});
|
||||
|
||||
test('displays applications', async ({ applicationsPage }) => {
|
||||
await applicationsPage.page.getByTestId('apps-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
await expect(applicationsPage.page.getByTestId('app-row')).not.toHaveCount(0);
|
||||
|
||||
await applicationsPage.screenshot({
|
||||
path: 'Applications.png',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('can add connection', () => {
|
||||
test.beforeEach(async ({ applicationsPage }) => {
|
||||
await expect(applicationsPage.addConnectionButton).toBeClickableLink();
|
||||
await applicationsPage.addConnectionButton.click();
|
||||
await applicationsPage
|
||||
.page
|
||||
.getByTestId('search-for-app-loader')
|
||||
.waitFor({ state: 'detached' });
|
||||
});
|
||||
|
||||
test('lists applications', async ({ applicationsPage }) => {
|
||||
const appListItemCount = await applicationsPage.page.getByTestId('app-list-item').count();
|
||||
expect(appListItemCount).toBeGreaterThan(10);
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('searches an application', async ({ applicationsPage }) => {
|
||||
await applicationsPage.page.getByTestId('search-for-app-text-field').fill('DeepL');
|
||||
await applicationsPage
|
||||
.page
|
||||
.getByTestId('search-for-app-loader')
|
||||
.waitFor({ state: 'detached' });
|
||||
|
||||
await expect(applicationsPage.page.getByTestId('app-list-item')).toHaveCount(1);
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('goes to app page to create a connection', async ({
|
||||
applicationsPage,
|
||||
}) => {
|
||||
// loading app, app config, app auth clients take time
|
||||
test.setTimeout(60000);
|
||||
|
||||
await applicationsPage.page.getByTestId('app-list-item').first().click();
|
||||
await expect(applicationsPage.page).toHaveURL('/app/deepl/connections/add?shared=false');
|
||||
await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeVisible();
|
||||
|
||||
await applicationsPage.clickAway();
|
||||
});
|
||||
|
||||
test('closes the dialog on backdrop click', async ({
|
||||
applicationsPage,
|
||||
}) => {
|
||||
await applicationsPage.page.getByTestId('app-list-item').first().click();
|
||||
await expect(applicationsPage.page).toHaveURL('/app/deepl/connections/add?shared=false');
|
||||
await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeVisible();
|
||||
await applicationsPage.clickAway();
|
||||
await expect(applicationsPage.page).toHaveURL('/app/deepl/connections');
|
||||
await expect(applicationsPage.page.getByTestId('add-app-connection-dialog')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,49 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Connections page', () => {
|
||||
test.beforeEach(async ({ page, connectionsPage }) => {
|
||||
await page.getByTestId('apps-page-drawer-link').click();
|
||||
await page.goto('/app/ntfy/connections');
|
||||
});
|
||||
|
||||
test('shows connections if any', async ({ page, connectionsPage }) => {
|
||||
await page.getByTestId('apps-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
await connectionsPage.screenshot({
|
||||
path: 'Connections.png',
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('can add connection', () => {
|
||||
test('has a button to open add connection dialog', async ({ page }) => {
|
||||
await expect(page.getByTestId('add-connection-button')).toBeClickableLink();
|
||||
});
|
||||
|
||||
test('add connection button takes user to add connection page', async ({
|
||||
page,
|
||||
connectionsPage,
|
||||
}) => {
|
||||
await connectionsPage.clickAddConnectionButton();
|
||||
await expect(page).toHaveURL('/app/ntfy/connections/add?shared=false');
|
||||
});
|
||||
|
||||
test('shows add connection dialog to create a new connection', async ({
|
||||
page,
|
||||
connectionsPage,
|
||||
}) => {
|
||||
await connectionsPage.clickAddConnectionButton();
|
||||
await expect(page).toHaveURL('/app/ntfy/connections/add?shared=false');
|
||||
await page.getByTestId('create-connection-button').click();
|
||||
await expect(
|
||||
page.getByTestId('create-connection-button')
|
||||
).not.toBeVisible();
|
||||
|
||||
await connectionsPage.screenshot({
|
||||
path: 'Ntfy connections after creating a connection.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Executions page', () => {
|
||||
test.beforeEach(async ({ page, executionsPage }) => {
|
||||
await page.getByTestId('executions-page-drawer-link').click();
|
||||
await page.getByTestId('execution-row').first().click();
|
||||
|
||||
await expect(page).toHaveURL(/\/executions\//);
|
||||
});
|
||||
|
||||
test('displays data in by default', async ({ page, executionsPage }) => {
|
||||
await expect(page.getByTestId('execution-step').last()).toBeVisible();
|
||||
await expect(page.getByTestId('execution-step')).toHaveCount(2);
|
||||
|
||||
await executionsPage.screenshot({
|
||||
path: 'Execution - data in.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('displays data out', async ({ page, executionsPage }) => {
|
||||
const executionStepCount = await page.getByTestId('execution-step').count();
|
||||
for (let i = 0; i < executionStepCount; i++) {
|
||||
await page.getByTestId('data-out-tab').nth(i).click();
|
||||
await expect(page.getByTestId('data-out-panel').nth(i)).toBeVisible();
|
||||
|
||||
await executionsPage.screenshot({
|
||||
path: `Execution - data out - ${i}.png`,
|
||||
animations: 'disabled',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('does not display error', async ({ page }) => {
|
||||
await expect(page.getByTestId('error-tab')).toBeHidden();
|
||||
});
|
||||
});
|
17
packages/e2e-tests/tests/executions/list-executions.spec.js
Normal file
17
packages/e2e-tests/tests/executions/list-executions.spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('Executions page', () => {
|
||||
test.beforeEach(async ({ page, executionsPage }) => {
|
||||
await page.getByTestId('executions-page-drawer-link').click();
|
||||
});
|
||||
|
||||
test('displays executions', async ({ page, executionsPage }) => {
|
||||
await page.getByTestId('executions-loader').waitFor({
|
||||
state: 'detached',
|
||||
});
|
||||
await expect(page.getByTestId('execution-row').first()).toBeVisible();
|
||||
|
||||
await executionsPage.screenshot({ path: 'Executions.png' });
|
||||
});
|
||||
});
|
206
packages/e2e-tests/tests/flow-editor/create-flow.spec.js
Normal file
206
packages/e2e-tests/tests/flow-editor/create-flow.spec.js
Normal file
@@ -0,0 +1,206 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test('Ensure creating a new flow works', async ({ page }) => {
|
||||
await page.getByTestId('create-flow-button').click();
|
||||
await expect(page).toHaveURL(/\/editor\/create/);
|
||||
await expect(page).toHaveURL(
|
||||
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/
|
||||
);
|
||||
})
|
||||
|
||||
test(
|
||||
'Create a new flow with a Scheduler step then an Ntfy step',
|
||||
async ({ flowEditorPage, page }) => {
|
||||
await test.step('create flow', async () => {
|
||||
await test.step('navigate to new flow page', async () => {
|
||||
await page.getByTestId('create-flow-button').click();
|
||||
await page.waitForURL(
|
||||
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/
|
||||
);
|
||||
});
|
||||
|
||||
await test.step('has two steps by default', async () => {
|
||||
await expect(page.getByTestId('flow-step')).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('setup Scheduler trigger', async () => {
|
||||
await test.step('choose app and event substep', async () => {
|
||||
await test.step('choose application', async () => {
|
||||
await flowEditorPage.appAutocomplete.click();
|
||||
await page
|
||||
.getByRole('option', { name: 'Scheduler' })
|
||||
.click();
|
||||
});
|
||||
|
||||
await test.step('choose and event', async () => {
|
||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||
await flowEditorPage.eventAutocomplete.click();
|
||||
await page
|
||||
.getByRole('option', { name: 'Every hour' })
|
||||
.click();
|
||||
});
|
||||
|
||||
await test.step('continue to next step', async () => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
await test.step('collapses the substep', async () => {
|
||||
await expect(flowEditorPage.appAutocomplete).not.toBeVisible();
|
||||
await expect(flowEditorPage.eventAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('set up a trigger', async () => {
|
||||
await test.step('choose "yes" in "trigger on weekends?"', async () => {
|
||||
await expect(flowEditorPage.trigger).toBeVisible();
|
||||
await flowEditorPage.trigger.click();
|
||||
await page.getByRole('option', { name: 'Yes' }).click();
|
||||
});
|
||||
|
||||
await test.step('continue to next step', async () => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
await test.step('collapses the substep', async () => {
|
||||
await expect(flowEditorPage.trigger).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('test trigger', async () => {
|
||||
await test.step('show sample output', async () => {
|
||||
await expect(flowEditorPage.testOuput).not.toBeVisible();
|
||||
await flowEditorPage.continueButton.click();
|
||||
await expect(flowEditorPage.testOuput).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Scheduler trigger test output.png',
|
||||
});
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('arrange Ntfy action', async () => {
|
||||
await test.step('choose app and event substep', async () => {
|
||||
await test.step('choose application', async () => {
|
||||
await flowEditorPage.appAutocomplete.click();
|
||||
await page.getByRole('option', { name: 'Ntfy' }).click();
|
||||
});
|
||||
|
||||
await test.step('choose an event', async () => {
|
||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||
await flowEditorPage.eventAutocomplete.click();
|
||||
await page
|
||||
.getByRole('option', { name: 'Send message' })
|
||||
.click();
|
||||
});
|
||||
|
||||
await test.step('continue to next step', async () => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
await test.step('collapses the substep', async () => {
|
||||
await expect(flowEditorPage.appAutocomplete).not.toBeVisible();
|
||||
await expect(flowEditorPage.eventAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('choose connection substep', async () => {
|
||||
await test.step('choose connection list item', async () => {
|
||||
await flowEditorPage.connectionAutocomplete.click();
|
||||
await page.getByRole('option').first().click();
|
||||
});
|
||||
|
||||
await test.step('continue to next step', async () => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
await test.step('collapses the substep', async () => {
|
||||
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('set up action substep', async () => {
|
||||
await test.step('fill topic and message body', async () => {
|
||||
await page
|
||||
.getByTestId('parameters.topic-power-input')
|
||||
.locator('[contenteditable]')
|
||||
.fill('Topic');
|
||||
await page
|
||||
.getByTestId('parameters.message-power-input')
|
||||
.locator('[contenteditable]')
|
||||
.fill('Message body');
|
||||
});
|
||||
|
||||
await test.step('continue to next step', async () => {
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
|
||||
await test.step('collapses the substep', async () => {
|
||||
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('test trigger substep', async () => {
|
||||
await test.step('show sample output', async () => {
|
||||
await expect(flowEditorPage.testOuput).not.toBeVisible();
|
||||
await page
|
||||
.getByTestId('flow-substep-continue-button')
|
||||
.first()
|
||||
.click();
|
||||
await expect(flowEditorPage.testOuput).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Ntfy action test output.png',
|
||||
});
|
||||
await flowEditorPage.continueButton.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('publish and unpublish', async () => {
|
||||
await test.step('publish flow', async () => {
|
||||
await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible();
|
||||
await expect(flowEditorPage.publishFlowButton).toBeVisible();
|
||||
await flowEditorPage.publishFlowButton.click();
|
||||
await expect(flowEditorPage.publishFlowButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('shows read-only sticky snackbar', async () => {
|
||||
await expect(flowEditorPage.infoSnackbar).toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Published flow.png',
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('unpublish from snackbar', async () => {
|
||||
await page
|
||||
.getByTestId('unpublish-flow-from-snackbar')
|
||||
.click();
|
||||
await expect(flowEditorPage.infoSnackbar).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('publish once again', async () => {
|
||||
await expect(flowEditorPage.publishFlowButton).toBeVisible();
|
||||
await flowEditorPage.publishFlowButton.click();
|
||||
await expect(flowEditorPage.publishFlowButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('unpublish from layout top bar', async () => {
|
||||
await expect(flowEditorPage.unpublishFlowButton).toBeVisible();
|
||||
await flowEditorPage.unpublishFlowButton.click();
|
||||
await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible();
|
||||
await flowEditorPage.screenshot({
|
||||
path: 'Unpublished flow.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('in layout', async () => {
|
||||
await test.step('can go back to flows page', async () => {
|
||||
await page.getByTestId('editor-go-back-button').click();
|
||||
await expect(page).toHaveURL('/flows');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@@ -0,0 +1,176 @@
|
||||
// @ts-check
|
||||
const { test, expect } = require('../../fixtures/index');
|
||||
|
||||
test.describe('User interface page', () => {
|
||||
test.beforeEach(async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.profileMenuButton.click();
|
||||
await userInterfacePage.adminMenuItem.click();
|
||||
await expect(userInterfacePage.page).toHaveURL(/\/admin-settings\/users/);
|
||||
await userInterfacePage.userInterfaceDrawerItem.click();
|
||||
await expect(userInterfacePage.page).toHaveURL(
|
||||
/\/admin-settings\/user-interface/
|
||||
);
|
||||
await userInterfacePage.page.waitForURL(/\/admin-settings\/user-interface/);
|
||||
});
|
||||
|
||||
test.describe('checks if the shown values are used', async () => {
|
||||
test('checks primary main color', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.primaryMainColorInput.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const initialPrimaryMainColor =
|
||||
await userInterfacePage.primaryMainColorInput.inputValue();
|
||||
const initialRgbColor = userInterfacePage.hexToRgb(
|
||||
initialPrimaryMainColor
|
||||
);
|
||||
await expect(userInterfacePage.updateButton).toHaveCSS(
|
||||
'background-color',
|
||||
initialRgbColor
|
||||
);
|
||||
});
|
||||
|
||||
test('checks primary dark color', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.primaryDarkColorInput.waitFor({
|
||||
state: 'attached',
|
||||
});
|
||||
const initialPrimaryDarkColor =
|
||||
await userInterfacePage.primaryDarkColorInput.inputValue();
|
||||
const initialRgbColor = userInterfacePage.hexToRgb(
|
||||
initialPrimaryDarkColor
|
||||
);
|
||||
await expect(userInterfacePage.appBar).toHaveCSS(
|
||||
'background-color',
|
||||
initialRgbColor
|
||||
);
|
||||
});
|
||||
|
||||
test('checks custom logo', async ({ userInterfacePage }) => {
|
||||
const initialLogoSvgCode =
|
||||
await userInterfacePage.logoSvgCodeInput.inputValue();
|
||||
const logoSrcAttribute = await userInterfacePage.customLogo.getAttribute(
|
||||
'src'
|
||||
);
|
||||
const svgCode = userInterfacePage.encodeSVG(initialLogoSvgCode);
|
||||
expect(logoSrcAttribute).toMatch(svgCode);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(
|
||||
'fill fields and check if the inputs reflect them properly',
|
||||
async () => {
|
||||
test('fill primary main color and check the color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryMainColorInput.fill('#FF5733');
|
||||
const rgbColor = userInterfacePage.hexToRgb('#FF5733');
|
||||
const button = await userInterfacePage.primaryMainColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
|
||||
});
|
||||
|
||||
test('fill primary dark color and check the color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryDarkColorInput.fill('#12F63F');
|
||||
const rgbColor = userInterfacePage.hexToRgb('#12F63F');
|
||||
const button = await userInterfacePage.primaryDarkColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
|
||||
});
|
||||
|
||||
test('fill primary light color and check the color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryLightColorInput.fill('#1D0BF5');
|
||||
const rgbColor = userInterfacePage.hexToRgb('#1D0BF5');
|
||||
const button = await userInterfacePage.primaryLightColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test.describe(
|
||||
'update form based on input values and check if the inputs still reflect them',
|
||||
async () => {
|
||||
test('update primary main color and check color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryMainColorInput.fill('#00adef');
|
||||
await userInterfacePage.updateButton.click();
|
||||
const rgbColor = userInterfacePage.hexToRgb('#00adef');
|
||||
const button = await userInterfacePage.primaryMainColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
|
||||
});
|
||||
|
||||
test('update primary dark color and check color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryDarkColorInput.fill('#222222');
|
||||
await userInterfacePage.updateButton.click();
|
||||
const rgbColor = userInterfacePage.hexToRgb('#222222');
|
||||
const button = await userInterfacePage.primaryDarkColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
|
||||
});
|
||||
|
||||
test('update primary light color and check color input', async ({
|
||||
userInterfacePage,
|
||||
}) => {
|
||||
await userInterfacePage.primaryLightColorInput.fill('#f90707');
|
||||
await userInterfacePage.updateButton.click();
|
||||
const rgbColor = userInterfacePage.hexToRgb('#f90707');
|
||||
const button = await userInterfacePage.primaryLightColorButton;
|
||||
const styleAttribute = await button.getAttribute('style');
|
||||
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
test.describe('update form based on input values', async () => {
|
||||
test('fill primary main color', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.primaryMainColorInput.fill('#00adef');
|
||||
await userInterfacePage.updateButton.click();
|
||||
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
|
||||
await userInterfacePage.screenshot({
|
||||
path: 'updated primary main color.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('fill primary dark color', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.primaryDarkColorInput.fill('#222222');
|
||||
await userInterfacePage.updateButton.click();
|
||||
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
|
||||
await userInterfacePage.screenshot({
|
||||
path: 'updated primary dark color.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('fill primary light color', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.primaryLightColorInput.fill('#f90707');
|
||||
await userInterfacePage.updateButton.click();
|
||||
await userInterfacePage.goToDashboardButton.click();
|
||||
await expect(userInterfacePage.page).toHaveURL('/flows');
|
||||
const span = await userInterfacePage.flowRowCardActionArea;
|
||||
await span.waitFor({ state: 'visible' });
|
||||
await span.hover();
|
||||
await userInterfacePage.screenshot({
|
||||
path: 'updated primary light color.png',
|
||||
});
|
||||
});
|
||||
|
||||
test('fill logo svg code', async ({ userInterfacePage }) => {
|
||||
await userInterfacePage.logoSvgCodeInput
|
||||
.fill(`<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
|
||||
<rect width="100%" height="100%" fill="white" />
|
||||
<text x="10" y="40" font-family="Arial" font-size="40" fill="black">A</text>
|
||||
</svg>`);
|
||||
await userInterfacePage.updateButton.click();
|
||||
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
|
||||
await userInterfacePage.screenshot({
|
||||
path: 'updated svg code.png',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
9
packages/types/index.d.ts
vendored
9
packages/types/index.d.ts
vendored
@@ -119,8 +119,8 @@ export interface IPermission {
|
||||
|
||||
export interface IPermissionCatalog {
|
||||
actions: { label: string; key: string; subjects: string[] }[];
|
||||
subjects: { label: string; key: string; }[];
|
||||
conditions: { label: string; key: string; }[];
|
||||
subjects: { label: string; key: string }[];
|
||||
conditions: { label: string; key: string }[];
|
||||
}
|
||||
|
||||
export interface IFieldDropdown {
|
||||
@@ -418,7 +418,7 @@ type TSamlAuthProvider = {
|
||||
id: string;
|
||||
name: string;
|
||||
certificate: string;
|
||||
signatureAlgorithm: "sha1" | "sha256" | "sha512";
|
||||
signatureAlgorithm: 'sha1' | 'sha256' | 'sha512';
|
||||
issuer: string;
|
||||
entryPoint: string;
|
||||
firstnameAttributeName: string;
|
||||
@@ -426,7 +426,8 @@ type TSamlAuthProvider = {
|
||||
emailAttributeName: string;
|
||||
roleAttributeName: string;
|
||||
defaultRoleId: string;
|
||||
}
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
type AppConfig = {
|
||||
id: string;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/types",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "Type definitions for automatisch",
|
||||
"homepage": "https://github.com/automatisch/automatisch",
|
||||
|
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@automatisch/web",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@automatisch/types": "^0.8.0",
|
||||
"@automatisch/types": "^0.9.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@casl/react": "^3.1.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
@@ -30,6 +30,7 @@
|
||||
"graphql": "^15.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.3.1",
|
||||
"mui-color-input": "^2.0.0",
|
||||
"notistack": "^2.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
@@ -6,6 +6,8 @@ import CreateUser from 'pages/CreateUser';
|
||||
import Roles from 'pages/Roles/index.ee';
|
||||
import CreateRole from 'pages/CreateRole/index.ee';
|
||||
import EditRole from 'pages/EditRole/index.ee';
|
||||
import Authentication from 'pages/Authentication';
|
||||
import UserInterface from 'pages/UserInterface';
|
||||
|
||||
import * as URLS from 'config/urls';
|
||||
import Can from 'components/Can';
|
||||
@@ -79,6 +81,32 @@ export default (
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.USER_INTERFACE}
|
||||
element={
|
||||
<Can I="update" a="Config">
|
||||
<AdminSettingsLayout>
|
||||
<UserInterface />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.AUTHENTICATION}
|
||||
element={
|
||||
<Can I="read" a="SamlAuthProvider">
|
||||
<Can I="update" a="SamlAuthProvider">
|
||||
<Can I="create" a="SamlAuthProvider">
|
||||
<AdminSettingsLayout>
|
||||
<Authentication />
|
||||
</AdminSettingsLayout>
|
||||
</Can>
|
||||
</Can>
|
||||
</Can>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={URLS.ADMIN_SETTINGS}
|
||||
element={<Navigate to={URLS.USERS} replace />}
|
||||
|
@@ -101,7 +101,9 @@ export default function AddNewAppConnection(
|
||||
</InputAdornment>
|
||||
}
|
||||
label={formatMessage('apps.searchApp')}
|
||||
data-test="search-for-app-text-field"
|
||||
inputProps={{
|
||||
'data-test': 'search-for-app-text-field',
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
@@ -109,7 +111,10 @@ export default function AddNewAppConnection(
|
||||
<DialogContent>
|
||||
<List sx={{ pt: 2, width: '100%' }}>
|
||||
{loading && (
|
||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||
<CircularProgress
|
||||
data-test="search-for-app-loader"
|
||||
sx={{ display: 'block', margin: '20px auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import GroupIcon from '@mui/icons-material/Group';
|
||||
import GroupsIcon from '@mui/icons-material/Groups';
|
||||
import LockIcon from '@mui/icons-material/LockPerson';
|
||||
import BrushIcon from '@mui/icons-material/Brush';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
@@ -18,25 +20,56 @@ type SettingsLayoutProps = {
|
||||
};
|
||||
|
||||
type DrawerLink = {
|
||||
Icon: SvgIconComponent,
|
||||
primary: string,
|
||||
to: string,
|
||||
}
|
||||
Icon: SvgIconComponent;
|
||||
primary: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
function createDrawerLinks({ canReadRole, canReadUser }: { canReadRole: boolean; canReadUser: boolean; }) {
|
||||
function createDrawerLinks({
|
||||
canReadRole,
|
||||
canReadUser,
|
||||
canUpdateConfig,
|
||||
canManageSamlAuthProvider,
|
||||
}: {
|
||||
canReadRole: boolean;
|
||||
canReadUser: boolean;
|
||||
canUpdateConfig: boolean;
|
||||
canManageSamlAuthProvider: boolean;
|
||||
}) {
|
||||
const items = [
|
||||
canReadUser ? {
|
||||
Icon: GroupIcon,
|
||||
primary: 'adminSettingsDrawer.users',
|
||||
to: URLS.USERS,
|
||||
} : null,
|
||||
canReadRole ? {
|
||||
Icon: GroupsIcon,
|
||||
primary: 'adminSettingsDrawer.roles',
|
||||
to: URLS.ROLES,
|
||||
} : null
|
||||
]
|
||||
.filter(Boolean) as DrawerLink[];
|
||||
canReadUser
|
||||
? {
|
||||
Icon: GroupIcon,
|
||||
primary: 'adminSettingsDrawer.users',
|
||||
to: URLS.USERS,
|
||||
dataTest: 'users-drawer-link',
|
||||
}
|
||||
: null,
|
||||
canReadRole
|
||||
? {
|
||||
Icon: GroupsIcon,
|
||||
primary: 'adminSettingsDrawer.roles',
|
||||
to: URLS.ROLES,
|
||||
dataTest: 'roles-drawer-link',
|
||||
}
|
||||
: null,
|
||||
canUpdateConfig
|
||||
? {
|
||||
Icon: BrushIcon,
|
||||
primary: 'adminSettingsDrawer.userInterface',
|
||||
to: URLS.USER_INTERFACE,
|
||||
dataTest: 'user-interface-drawer-link',
|
||||
}
|
||||
: null,
|
||||
canManageSamlAuthProvider
|
||||
? {
|
||||
Icon: LockIcon,
|
||||
primary: 'adminSettingsDrawer.authentication',
|
||||
to: URLS.AUTHENTICATION,
|
||||
dataTest: 'authentication-drawer-link',
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as DrawerLink[];
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -46,6 +79,7 @@ const drawerBottomLinks = [
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: 'adminSettingsDrawer.goBack',
|
||||
to: '/',
|
||||
dataTest: 'go-back-drawer-link',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -62,6 +96,11 @@ export default function SettingsLayout({
|
||||
const drawerLinks = createDrawerLinks({
|
||||
canReadUser: currentUserAbility.can('read', 'User'),
|
||||
canReadRole: currentUserAbility.can('read', 'Role'),
|
||||
canUpdateConfig: currentUserAbility.can('update', 'Config'),
|
||||
canManageSamlAuthProvider:
|
||||
currentUserAbility.can('read', 'SamlAuthProvider') &&
|
||||
currentUserAbility.can('update', 'SamlAuthProvider') &&
|
||||
currentUserAbility.can('create', 'SamlAuthProvider'),
|
||||
});
|
||||
|
||||
return (
|
||||
|
@@ -46,7 +46,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiAppBar>
|
||||
<MuiAppBar data-test="app-bar">
|
||||
<Container maxWidth={maxWidth} disableGutters>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
|
40
packages/web/src/components/ColorInput/ColorButton/index.tsx
Normal file
40
packages/web/src/components/ColorInput/ColorButton/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { ButtonProps } from '@mui/material/Button';
|
||||
import { Button } from './style';
|
||||
|
||||
const BG_IMAGE_FALLBACK =
|
||||
'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%) /*! @noflip */';
|
||||
|
||||
export type ColorButtonProps = Omit<ButtonProps, 'children'> & {
|
||||
bgColor: string;
|
||||
isBgColorValid: boolean;
|
||||
disablePopover: boolean;
|
||||
};
|
||||
|
||||
export type ColorButtonElement = (props: ColorButtonProps) => JSX.Element;
|
||||
|
||||
const ColorButton = (props: ColorButtonProps) => {
|
||||
const {
|
||||
bgColor,
|
||||
className,
|
||||
disablePopover,
|
||||
isBgColorValid,
|
||||
...restButtonProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-test="color-button"
|
||||
disableTouchRipple
|
||||
style={{
|
||||
backgroundColor: isBgColorValid ? bgColor : undefined,
|
||||
backgroundImage: isBgColorValid ? undefined : BG_IMAGE_FALLBACK,
|
||||
cursor: disablePopover ? 'default' : undefined,
|
||||
}}
|
||||
className={`MuiColorInput-Button ${className || ''}`}
|
||||
{...restButtonProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorButton;
|
15
packages/web/src/components/ColorInput/ColorButton/style.tsx
Normal file
15
packages/web/src/components/ColorInput/ColorButton/style.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import MuiButton from '@mui/material/Button';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const Button = styled(MuiButton)(() => ({
|
||||
backgroundSize: '8px 8px',
|
||||
backgroundPosition: '0 0, 4px 0, 4px -4px, 0px 4px',
|
||||
transition: 'none',
|
||||
boxShadow: '0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08)',
|
||||
border: 0,
|
||||
borderRadius: 4,
|
||||
width: '24px',
|
||||
aspectRatio: '1 / 1',
|
||||
height: '24px',
|
||||
minWidth: 0,
|
||||
})) as typeof MuiButton;
|
42
packages/web/src/components/ColorInput/index.tsx
Normal file
42
packages/web/src/components/ColorInput/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { MuiColorInput, MuiColorInputProps } from 'mui-color-input';
|
||||
import ColorButton from './ColorButton';
|
||||
|
||||
type ColorInputProps = {
|
||||
shouldUnregister?: boolean;
|
||||
name: string;
|
||||
'data-test'?: string;
|
||||
} & Partial<MuiColorInputProps>;
|
||||
|
||||
export default function ColorInput(props: ColorInputProps): React.ReactElement {
|
||||
const { control } = useFormContext();
|
||||
const {
|
||||
required,
|
||||
name,
|
||||
shouldUnregister = false,
|
||||
disabled = false,
|
||||
...textFieldProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
rules={{ required }}
|
||||
name={name}
|
||||
control={control}
|
||||
shouldUnregister={shouldUnregister}
|
||||
render={({ field }) => (
|
||||
<MuiColorInput
|
||||
Adornment={ColorButton}
|
||||
format="hex"
|
||||
{...textFieldProps}
|
||||
{...field}
|
||||
disabled={disabled}
|
||||
inputProps={{
|
||||
'data-test': 'color-text-field',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -20,7 +20,7 @@ interface CustomOptionsProps {
|
||||
onTabChange: (tabIndex: 0 | 1) => void;
|
||||
label?: string;
|
||||
initialTabIndex?: 0 | 1;
|
||||
};
|
||||
}
|
||||
|
||||
const CustomOptions = (props: CustomOptionsProps) => {
|
||||
const {
|
||||
@@ -34,17 +34,23 @@ const CustomOptions = (props: CustomOptionsProps) => {
|
||||
label,
|
||||
initialTabIndex,
|
||||
} = props;
|
||||
const [activeTabIndex, setActiveTabIndex] = React.useState<number | undefined>(undefined);
|
||||
|
||||
React.useEffect(function applyInitialActiveTabIndex() {
|
||||
setActiveTabIndex((currentActiveTabIndex) => {
|
||||
if (currentActiveTabIndex === undefined) {
|
||||
return initialTabIndex;
|
||||
}
|
||||
const [activeTabIndex, setActiveTabIndex] = React.useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
return currentActiveTabIndex;
|
||||
});
|
||||
}, [initialTabIndex]);
|
||||
React.useEffect(
|
||||
function applyInitialActiveTabIndex() {
|
||||
setActiveTabIndex((currentActiveTabIndex) => {
|
||||
if (currentActiveTabIndex === undefined) {
|
||||
return initialTabIndex;
|
||||
}
|
||||
|
||||
return currentActiveTabIndex;
|
||||
});
|
||||
},
|
||||
[initialTabIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popper
|
||||
@@ -75,22 +81,15 @@ const CustomOptions = (props: CustomOptionsProps) => {
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={activeTabIndex ?? 0} index={0}>
|
||||
<Options
|
||||
data={options}
|
||||
onOptionClick={onOptionClick}
|
||||
/>
|
||||
<Options data={options} onOptionClick={onOptionClick} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTabIndex ?? 0} index={1}>
|
||||
<Suggestions
|
||||
data={data}
|
||||
onSuggestionClick={onSuggestionClick}
|
||||
/>
|
||||
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CustomOptions;
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { useController, useFormContext } from 'react-hook-form';
|
||||
import { IconButton } from '@mui/material';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||
import { FakeDropdownButton } from './style';
|
||||
import { ActionButtonsWrapper } from './style';
|
||||
|
||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { createEditor } from 'slate';
|
||||
import { Editable, ReactEditor,} from 'slate-react';
|
||||
import { Editable, ReactEditor } from 'slate-react';
|
||||
|
||||
import Slate from 'components/Slate';
|
||||
import Element from 'components/Slate/Element';
|
||||
@@ -23,7 +25,11 @@ import {
|
||||
overrideEditorValue,
|
||||
focusEditor,
|
||||
} from 'components/Slate/utils';
|
||||
import { FakeInput, InputLabelWrapper, ChildrenWrapper, } from 'components/PowerInput/style';
|
||||
import {
|
||||
FakeInput,
|
||||
InputLabelWrapper,
|
||||
ChildrenWrapper,
|
||||
} from 'components/PowerInput/style';
|
||||
import { VariableElement } from 'components/Slate/types';
|
||||
import CustomOptions from './CustomOptions';
|
||||
import { processStepWithExecutions } from 'components/PowerInput/data';
|
||||
@@ -75,9 +81,11 @@ function ControlledCustomAutocomplete(
|
||||
onChange: controllerOnChange,
|
||||
onBlur: controllerOnBlur,
|
||||
} = field;
|
||||
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
||||
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
||||
const [isInitialValueSet, setInitialValue] = React.useState(false);
|
||||
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(undefined);
|
||||
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const renderElement = React.useCallback(
|
||||
@@ -104,12 +112,12 @@ function ControlledCustomAutocomplete(
|
||||
const promoteValue = () => {
|
||||
const serializedValue = serialize(editor.children);
|
||||
controllerOnChange(serializedValue);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
|
||||
return new ResizeObserver(() => {
|
||||
forceUpdate();
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -121,24 +129,37 @@ function ControlledCustomAutocomplete(
|
||||
}
|
||||
}, dependsOnValues);
|
||||
|
||||
React.useEffect(function updateInitialValue() {
|
||||
const hasOptions = options.length;
|
||||
const isOptionsLoaded = loading === false;
|
||||
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
|
||||
setInitialValue(true);
|
||||
React.useEffect(
|
||||
function updateInitialValue() {
|
||||
const hasOptions = options.length;
|
||||
const isOptionsLoaded = loading === false;
|
||||
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
|
||||
setInitialValue(true);
|
||||
|
||||
const option: IFieldDropdownOption | undefined = options.find((option) => option.value === value);
|
||||
const option: IFieldDropdownOption | undefined = options.find(
|
||||
(option) => option.value === value
|
||||
);
|
||||
|
||||
if (option) {
|
||||
overrideEditorValue(editor, { option, focus: false });
|
||||
setSingleChoice(true);
|
||||
} else if (value) {
|
||||
setSingleChoice(false);
|
||||
if (option) {
|
||||
overrideEditorValue(editor, { option, focus: false });
|
||||
setSingleChoice(true);
|
||||
} else if (value) {
|
||||
setSingleChoice(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isInitialValueSet, options, loading]);
|
||||
},
|
||||
[isInitialValueSet, options, loading]
|
||||
);
|
||||
|
||||
const hideSuggestionsOnShift = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
React.useEffect(() => {
|
||||
if (!showVariableSuggestions && value !== serialize(editor.children)) {
|
||||
promoteValue();
|
||||
}
|
||||
}, [showVariableSuggestions]);
|
||||
|
||||
const hideSuggestionsOnShift = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.code === 'Tab') {
|
||||
setShowVariableSuggestions(false);
|
||||
}
|
||||
@@ -170,21 +191,26 @@ function ControlledCustomAutocomplete(
|
||||
(event: React.MouseEvent, option: IFieldDropdownOption) => {
|
||||
event.stopPropagation();
|
||||
overrideEditorValue(editor, { option, focus: false });
|
||||
|
||||
setShowVariableSuggestions(false);
|
||||
|
||||
promoteValue();
|
||||
setSingleChoice(true);
|
||||
},
|
||||
[stepsWithVariables]
|
||||
);
|
||||
|
||||
const handleClearButtonClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
resetEditor(editor);
|
||||
promoteValue();
|
||||
setSingleChoice(undefined);
|
||||
};
|
||||
|
||||
const reset = (tabIndex: 0 | 1) => {
|
||||
const isOptions = tabIndex === 0;
|
||||
|
||||
setSingleChoice(isOptions);
|
||||
|
||||
resetEditor(editor, { focus: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Slate
|
||||
@@ -193,11 +219,7 @@ function ControlledCustomAutocomplete(
|
||||
>
|
||||
<ClickAwayListener
|
||||
mouseEvent="onMouseDown"
|
||||
onClickAway={() => {
|
||||
promoteValue();
|
||||
|
||||
setShowVariableSuggestions(false);
|
||||
}}
|
||||
onClickAway={() => setShowVariableSuggestions(false)}
|
||||
>
|
||||
{/* ref-able single child for ClickAwayListener */}
|
||||
<ChildrenWrapper style={{ width: '100%' }} data-test="power-input">
|
||||
@@ -232,14 +254,27 @@ function ControlledCustomAutocomplete(
|
||||
}}
|
||||
/>
|
||||
|
||||
<FakeDropdownButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</FakeDropdownButton>
|
||||
<ActionButtonsWrapper direction="row" mr={1.5}>
|
||||
{isSingleChoice && serialize(editor.children) && (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
onClick={handleClearButtonClick}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</IconButton>
|
||||
</ActionButtonsWrapper>
|
||||
</FakeInput>
|
||||
{/* ghost placer for the variables popover */}
|
||||
<div
|
||||
@@ -247,14 +282,16 @@ function ControlledCustomAutocomplete(
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
left: 16
|
||||
left: 16,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CustomOptions
|
||||
label={label}
|
||||
open={showVariableSuggestions}
|
||||
initialTabIndex={isSingleChoice === undefined ? undefined : (isSingleChoice ? 0 : 1)}
|
||||
initialTabIndex={
|
||||
isSingleChoice === undefined ? undefined : isSingleChoice ? 0 : 1
|
||||
}
|
||||
anchorEl={editorRef.current}
|
||||
data={stepsWithVariables}
|
||||
options={options}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiIconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTabs from '@mui/material/Tabs';
|
||||
|
||||
export const FakeDropdownButton = styled(MuiIconButton)`
|
||||
export const ActionButtonsWrapper = styled(Stack)`
|
||||
position: absolute;
|
||||
right: ${({ theme }) => theme.spacing(1)};
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import { LogoImage } from './style.ee';
|
||||
|
||||
const CustomLogo = () => {
|
||||
const { config, loading } = useConfig(['logo.svgData']);
|
||||
@@ -8,7 +9,10 @@ const CustomLogo = () => {
|
||||
const logoSvgData = config['logo.svgData'] as string;
|
||||
|
||||
return (
|
||||
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(logoSvgData)}`} />
|
||||
<LogoImage
|
||||
data-test="custom-logo"
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(logoSvgData)}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
8
packages/web/src/components/CustomLogo/style.ee.ts
Normal file
8
packages/web/src/components/CustomLogo/style.ee.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const LogoImage = styled('img')(() => ({
|
||||
maxWidth: 200,
|
||||
maxHeight: 50,
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}));
|
@@ -68,19 +68,22 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
</div>
|
||||
|
||||
<List sx={{ py: 0, mt: 3 }}>
|
||||
{bottomLinks.map(({ Icon, badgeContent, primary, to }, index) => (
|
||||
<ListItemLink
|
||||
key={`${to}-${index}`}
|
||||
icon={
|
||||
<Badge badgeContent={badgeContent} color="secondary" max={99}>
|
||||
<Icon htmlColor={theme.palette.primary.main} />
|
||||
</Badge>
|
||||
}
|
||||
primary={formatMessage(primary)}
|
||||
to={to}
|
||||
onClick={closeOnClick}
|
||||
/>
|
||||
))}
|
||||
{bottomLinks.map(
|
||||
({ Icon, badgeContent, primary, to, dataTest }, index) => (
|
||||
<ListItemLink
|
||||
key={`${to}-${index}`}
|
||||
icon={
|
||||
<Badge badgeContent={badgeContent} color="secondary" max={99}>
|
||||
<Icon htmlColor={theme.palette.primary.main} />
|
||||
</Badge>
|
||||
}
|
||||
primary={formatMessage(primary)}
|
||||
to={to}
|
||||
onClick={closeOnClick}
|
||||
data-test={dataTest}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</BaseDrawer>
|
||||
);
|
||||
|
@@ -6,6 +6,8 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { IExecution } from '@automatisch/types';
|
||||
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
type ExecutionHeaderProps = {
|
||||
execution: IExecution;
|
||||
};
|
||||
@@ -19,13 +21,18 @@ function ExecutionName(props: Pick<IExecution['flow'], 'name'>) {
|
||||
}
|
||||
|
||||
function ExecutionId(props: Pick<IExecution, 'id'>) {
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const id = (
|
||||
<Typography variant="body1" component="span">
|
||||
{props.id}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Typography variant="body2">
|
||||
Execution ID:{' '}
|
||||
<Typography variant="body1" component="span">
|
||||
{props.id}
|
||||
</Typography>
|
||||
{formatMessage('execution.id', { id })}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
AppIconStatusIconWrapper,
|
||||
Content,
|
||||
Header,
|
||||
Metadata,
|
||||
Wrapper,
|
||||
} from './style';
|
||||
|
||||
@@ -31,16 +32,36 @@ type ExecutionStepProps = {
|
||||
executionStep: IExecutionStep;
|
||||
};
|
||||
|
||||
function ExecutionStepId(props: Pick<IExecutionStep, 'id'>) {
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const id = (
|
||||
<Typography variant="caption" component="span">
|
||||
{props.id}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }} gridArea="id">
|
||||
<Typography variant="caption" fontWeight="bold">
|
||||
{formatMessage('executionStep.id', { id })}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutionStepDate(props: Pick<IExecutionStep, 'createdAt'>) {
|
||||
const formatMessage = useFormatMessage();
|
||||
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
||||
const relativeCreatedAt = createdAt.toRelative();
|
||||
|
||||
return (
|
||||
<Tooltip title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}>
|
||||
<Tooltip
|
||||
title={createdAt.toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
||||
>
|
||||
<Typography variant="caption" gutterBottom>
|
||||
{formatMessage('executionStep.executedAt', {
|
||||
datetime: relativeCreatedAt
|
||||
datetime: relativeCreatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
@@ -74,30 +95,37 @@ export default function ExecutionStep(
|
||||
return (
|
||||
<Wrapper elevation={1} data-test="execution-step">
|
||||
<Header>
|
||||
<Stack direction="row" gap={2}>
|
||||
<Stack direction="row" gap={3}>
|
||||
<AppIconWrapper>
|
||||
<AppIcon url={app?.iconUrl} name={app?.name} />
|
||||
|
||||
<AppIconStatusIconWrapper>
|
||||
<AppIcon url={app?.iconUrl} name={app?.name} />
|
||||
|
||||
{validationStatusIcon}
|
||||
</AppIconStatusIconWrapper>
|
||||
</AppIconWrapper>
|
||||
|
||||
<Box flex="1">
|
||||
<Typography variant="caption">
|
||||
{isTrigger
|
||||
? formatMessage('flowStep.triggerType')
|
||||
: formatMessage('flowStep.actionType')}
|
||||
</Typography>
|
||||
<Metadata flex="1">
|
||||
<ExecutionStepId id={executionStep.step.id} />
|
||||
|
||||
<Typography variant="body2">
|
||||
{step.position}. {app?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box flex="1" gridArea="step">
|
||||
<Typography variant="caption">
|
||||
{isTrigger && formatMessage('flowStep.triggerType')}
|
||||
{isAction && formatMessage('flowStep.actionType')}
|
||||
</Typography>
|
||||
|
||||
<Box alignSelf="flex-end">
|
||||
<ExecutionStepDate createdAt={executionStep.createdAt} />
|
||||
</Box>
|
||||
<Typography variant="body2">
|
||||
{step.position}. {app?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent={["left", "right"]}
|
||||
gridArea="date"
|
||||
>
|
||||
<ExecutionStepDate createdAt={executionStep.createdAt} />
|
||||
</Box>
|
||||
</Metadata>
|
||||
</Stack>
|
||||
</Header>
|
||||
|
||||
@@ -117,7 +145,7 @@ export default function ExecutionStep(
|
||||
<SearchableJSONViewer data={executionStep.dataIn} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTabIndex} index={1}>
|
||||
<TabPanel value={activeTabIndex} index={1} data-test="data-out-panel">
|
||||
<SearchableJSONViewer data={executionStep.dataOut} />
|
||||
</TabPanel>
|
||||
|
||||
|
@@ -1,18 +1,22 @@
|
||||
import { styled, alpha } from '@mui/material/styles';
|
||||
import Card from '@mui/material/Card';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
export const AppIconWrapper = styled('div')`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
|
||||
export const AppIconStatusIconWrapper = styled('span')`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translate(50%, -50%);
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transform: translate(50%, -50%);
|
||||
// to make it distinguishable over an app icon
|
||||
background: white;
|
||||
border-radius: 100%;
|
||||
@@ -31,7 +35,7 @@ type HeaderProps = {
|
||||
|
||||
export const Header = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'collapsed',
|
||||
})<HeaderProps>`
|
||||
}) <HeaderProps>`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
||||
`;
|
||||
@@ -42,3 +46,20 @@ export const Content = styled('div')`
|
||||
border-right: none;
|
||||
padding: ${({ theme }) => theme.spacing(2, 0)};
|
||||
`;
|
||||
|
||||
export const Metadata = styled(Box)`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"step id"
|
||||
"step date";
|
||||
|
||||
${({ theme }) => theme.breakpoints.down('sm')} {
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"id"
|
||||
"step"
|
||||
"date";
|
||||
}
|
||||
` as typeof Box;
|
||||
|
@@ -18,7 +18,7 @@ type FlowRowProps = {
|
||||
flow: IFlow;
|
||||
};
|
||||
|
||||
function getFlowStatusTranslationKey(status: IFlow["status"]): string {
|
||||
function getFlowStatusTranslationKey(status: IFlow['status']): string {
|
||||
if (status === 'published') {
|
||||
return 'flow.published';
|
||||
} else if (status === 'paused') {
|
||||
@@ -28,7 +28,16 @@ function getFlowStatusTranslationKey(status: IFlow["status"]): string {
|
||||
return 'flow.draft';
|
||||
}
|
||||
|
||||
function getFlowStatusColor(status: IFlow["status"]): 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' {
|
||||
function getFlowStatusColor(
|
||||
status: IFlow['status']
|
||||
):
|
||||
| 'default'
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'error'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'warning' {
|
||||
if (status === 'published') {
|
||||
return 'success';
|
||||
} else if (status === 'paused') {
|
||||
@@ -64,8 +73,12 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ mb: 1 }}>
|
||||
<CardActionArea component={Link} to={URLS.FLOW(flow.id)}>
|
||||
<Card sx={{ mb: 1 }} data-test="flow-row">
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={URLS.FLOW(flow.id)}
|
||||
data-test="card-action-area"
|
||||
>
|
||||
<CardContent>
|
||||
<Apps direction="row" gap={1} sx={{ gridArea: 'apps' }}>
|
||||
<FlowAppIcons steps={flow.steps} />
|
||||
@@ -98,9 +111,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
size="small"
|
||||
color={getFlowStatusColor(flow?.status)}
|
||||
variant={flow?.active ? 'filled' : 'outlined'}
|
||||
label={formatMessage(
|
||||
getFlowStatusTranslationKey(flow?.status)
|
||||
)}
|
||||
label={formatMessage(getFlowStatusTranslationKey(flow?.status))}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
|
@@ -71,17 +71,18 @@ function generateValidationSchema(substeps: ISubstep[]) {
|
||||
substepArgumentValidations[key] = yup.mixed();
|
||||
}
|
||||
|
||||
if (typeof substepArgumentValidations[key] === 'object' && (arg.type === 'string' || arg.type === 'dropdown')) {
|
||||
if (
|
||||
typeof substepArgumentValidations[key] === 'object' &&
|
||||
(arg.type === 'string' || arg.type === 'dropdown')
|
||||
) {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
substepArgumentValidations[key] = substepArgumentValidations[
|
||||
key
|
||||
]
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key]
|
||||
.required(`${key} is required.`)
|
||||
.test(
|
||||
'empty-check',
|
||||
`${key} must be not empty`,
|
||||
(value: any) => !isEmpty(value),
|
||||
(value: any) => !isEmpty(value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +167,9 @@ export default function FlowStep(
|
||||
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
||||
(isTrigger ? app?.triggers : app?.actions) || [];
|
||||
const actionOrTrigger = actionsOrTriggers?.find(({ key }) => key === step.key);
|
||||
const actionOrTrigger = actionsOrTriggers?.find(
|
||||
({ key }) => key === step.key
|
||||
);
|
||||
const substeps = actionOrTrigger?.substeps || [];
|
||||
|
||||
const handleChange = React.useCallback(({ step }: { step: IStep }) => {
|
||||
@@ -187,7 +190,12 @@ export default function FlowStep(
|
||||
);
|
||||
|
||||
if (!apps) {
|
||||
return <CircularProgress sx={{ display: 'block', my: 2 }} />;
|
||||
return (
|
||||
<CircularProgress
|
||||
data-test="step-circular-loader"
|
||||
sx={{ display: 'block', my: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onContextMenuClose = (event: React.SyntheticEvent) => {
|
||||
@@ -279,7 +287,8 @@ export default function FlowStep(
|
||||
step={step}
|
||||
/>
|
||||
|
||||
{actionOrTrigger && substeps?.length > 0 &&
|
||||
{actionOrTrigger &&
|
||||
substeps?.length > 0 &&
|
||||
substeps.map((substep: ISubstep, index: number) => (
|
||||
<React.Fragment key={`${substep?.name}-${index}`}>
|
||||
{substep.key === 'chooseConnection' && app && (
|
||||
@@ -304,7 +313,11 @@ export default function FlowStep(
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
onContinue={onContinue}
|
||||
showWebhookUrl={'showWebhookUrl' in actionOrTrigger ? actionOrTrigger.showWebhookUrl : false}
|
||||
showWebhookUrl={
|
||||
'showWebhookUrl' in actionOrTrigger
|
||||
? actionOrTrigger.showWebhookUrl
|
||||
: false
|
||||
}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
|
@@ -9,12 +9,12 @@ const Logo = () => {
|
||||
const { config, loading } = useConfig(['logo.svgData']);
|
||||
|
||||
const logoSvgData = config?.['logo.svgData'] as string;
|
||||
if (loading && !logoSvgData) return (<React.Fragment />);
|
||||
if (loading && !logoSvgData) return <React.Fragment />;
|
||||
|
||||
if (logoSvgData) return <CustomLogo />;
|
||||
|
||||
return (
|
||||
<Typography variant="h6" component="h1" noWrap>
|
||||
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
|
||||
<FormattedMessage id="brandText" />
|
||||
</Typography>
|
||||
);
|
||||
|
@@ -60,11 +60,13 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||
React.useState(false);
|
||||
|
||||
const disappearSuggestionsOnShift = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const disappearSuggestionsOnShift = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.code === 'Tab') {
|
||||
setShowVariableSuggestions(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stepsWithVariables = React.useMemo(() => {
|
||||
return processStepWithExecutions(priorStepsWithExecutions);
|
||||
@@ -112,7 +114,10 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
}}
|
||||
>
|
||||
{/* ref-able single child for ClickAwayListener */}
|
||||
<ChildrenWrapper style={{ width: '100%' }} data-test="power-input">
|
||||
<ChildrenWrapper
|
||||
style={{ width: '100%' }}
|
||||
data-test={`${name}-power-input`}
|
||||
>
|
||||
<FakeInput disabled={disabled}>
|
||||
<InputLabelWrapper>
|
||||
<InputLabel
|
||||
@@ -140,7 +145,10 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
/>
|
||||
</FakeInput>
|
||||
{/* ghost placer for the variables popover */}
|
||||
<div ref={editorRef} style={{ position: 'absolute', right: 16, left: 16 }} />
|
||||
<div
|
||||
ref={editorRef}
|
||||
style={{ position: 'absolute', right: 16, left: 16 }}
|
||||
/>
|
||||
|
||||
<Popper
|
||||
open={showVariableSuggestions}
|
||||
|
@@ -9,4 +9,4 @@ export default function Element(props: any) {
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -3,7 +3,13 @@ import { withHistory } from 'slate-history';
|
||||
import { ReactEditor, withReact } from 'slate-react';
|
||||
import { IFieldDropdownOption } from '@automatisch/types';
|
||||
|
||||
import type { CustomEditor, CustomElement, CustomText, ParagraphElement, VariableElement } from './types';
|
||||
import type {
|
||||
CustomEditor,
|
||||
CustomElement,
|
||||
CustomText,
|
||||
ParagraphElement,
|
||||
VariableElement,
|
||||
} from './types';
|
||||
|
||||
type StepWithVariables = {
|
||||
id: string;
|
||||
@@ -13,7 +19,7 @@ type StepWithVariables = {
|
||||
sampleValue: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
type StepsWithVariables = StepWithVariables[];
|
||||
|
||||
@@ -26,10 +32,7 @@ function isCustomText(value: any): value is CustomText {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getStepPosition(
|
||||
id: string,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) {
|
||||
function getStepPosition(id: string, stepsWithVariables: StepsWithVariables) {
|
||||
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
|
||||
return stepWithVariables.id === id;
|
||||
});
|
||||
@@ -48,29 +51,36 @@ function getVariableStepId(variable: string) {
|
||||
return stepId;
|
||||
}
|
||||
|
||||
function getVariableSampleValue(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||
function getVariableSampleValue(
|
||||
variable: string,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) {
|
||||
const variableStepId = getVariableStepId(variable);
|
||||
const stepWithVariables = stepsWithVariables.find(({ id }: { id: string }) => id === variableStepId);
|
||||
const stepWithVariables = stepsWithVariables.find(
|
||||
({ id }: { id: string }) => id === variableStepId
|
||||
);
|
||||
|
||||
if (!stepWithVariables) return null;
|
||||
|
||||
const variableName = getVariableName(variable);
|
||||
const variableData = stepWithVariables.output.find(({ value }) => variableName === value);
|
||||
const variableData = stepWithVariables.output.find(
|
||||
({ value }) => variableName === value
|
||||
);
|
||||
|
||||
if (!variableData) return null;
|
||||
|
||||
return variableData.sampleValue;
|
||||
}
|
||||
|
||||
function getVariableDetails(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||
function getVariableDetails(
|
||||
variable: string,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) {
|
||||
const variableName = getVariableName(variable);
|
||||
const stepId = getVariableStepId(variableName);
|
||||
const stepPosition = getStepPosition(stepId, stepsWithVariables);
|
||||
const sampleValue = getVariableSampleValue(variable, stepsWithVariables);
|
||||
const label = variableName.replace(
|
||||
`step.${stepId}.`,
|
||||
`step${stepPosition}.`
|
||||
);
|
||||
const label = variableName.replace(`step.${stepId}.`, `step${stepPosition}.`);
|
||||
|
||||
return {
|
||||
sampleValue,
|
||||
@@ -114,7 +124,10 @@ export const deserialize = (
|
||||
type: 'paragraph',
|
||||
children: nodes.map((node) => {
|
||||
if (node.match(variableRegExp)) {
|
||||
const variableDetails = getVariableDetails(node, stepsWithVariables);
|
||||
const variableDetails = getVariableDetails(
|
||||
node,
|
||||
stepsWithVariables
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'variable',
|
||||
@@ -199,7 +212,10 @@ export const insertVariable = (
|
||||
variableData: Record<string, unknown>,
|
||||
stepsWithVariables: StepsWithVariables
|
||||
) => {
|
||||
const variableDetails = getVariableDetails(`{{${variableData.value}}}`, stepsWithVariables);
|
||||
const variableDetails = getVariableDetails(
|
||||
`{{${variableData.value}}}`,
|
||||
stepsWithVariables
|
||||
);
|
||||
|
||||
const variable: VariableElement = {
|
||||
type: 'variable',
|
||||
@@ -217,15 +233,18 @@ export const insertVariable = (
|
||||
export const focusEditor = (editor: CustomEditor) => {
|
||||
ReactEditor.focus(editor);
|
||||
editor.move();
|
||||
}
|
||||
};
|
||||
|
||||
export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) => {
|
||||
export const resetEditor = (
|
||||
editor: CustomEditor,
|
||||
options?: { focus: boolean }
|
||||
) => {
|
||||
const focus = options?.focus || false;
|
||||
|
||||
editor.removeNodes({
|
||||
at: {
|
||||
anchor: editor.start([]),
|
||||
focus: editor.end([])
|
||||
focus: editor.end([]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -235,9 +254,12 @@ export const resetEditor = (editor: CustomEditor, options?: { focus: boolean })
|
||||
if (focus) {
|
||||
focusEditor(editor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const overrideEditorValue = (editor: CustomEditor, options: { option: IFieldDropdownOption, focus: boolean }) => {
|
||||
export const overrideEditorValue = (
|
||||
editor: CustomEditor,
|
||||
options: { option: IFieldDropdownOption; focus: boolean }
|
||||
) => {
|
||||
const { option, focus } = options;
|
||||
|
||||
const variable: ParagraphElement = {
|
||||
@@ -245,8 +267,8 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi
|
||||
children: [
|
||||
{
|
||||
value: option.value as string,
|
||||
text: option.label as string
|
||||
}
|
||||
text: option.label as string,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -254,7 +276,7 @@ export const overrideEditorValue = (editor: CustomEditor, options: { option: IFi
|
||||
editor.removeNodes({
|
||||
at: {
|
||||
anchor: editor.start([]),
|
||||
focus: editor.end([])
|
||||
focus: editor.end([]),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -270,9 +292,9 @@ export const createTextNode = (text: string): ParagraphElement => ({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text
|
||||
}
|
||||
]
|
||||
text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
||||
|
@@ -28,7 +28,7 @@ function SsoProviders() {
|
||||
variant="outlined"
|
||||
>
|
||||
{formatMessage('ssoProviders.loginWithProvider', {
|
||||
providerName: provider.name
|
||||
providerName: provider.name,
|
||||
})}
|
||||
</Button>
|
||||
))}
|
||||
|
74
packages/web/src/components/Switch/index.tsx
Normal file
74
packages/web/src/components/Switch/index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import FormControlLabel, {
|
||||
FormControlLabelProps,
|
||||
} from '@mui/material/FormControlLabel';
|
||||
import MuiSwitch, { SwitchProps as MuiSwitchProps } from '@mui/material/Switch';
|
||||
|
||||
type SwitchProps = {
|
||||
name: string;
|
||||
label: string;
|
||||
shouldUnregister?: boolean;
|
||||
FormControlLabelProps?: Partial<FormControlLabelProps>;
|
||||
} & MuiSwitchProps;
|
||||
|
||||
export default function Switch(props: SwitchProps): React.ReactElement {
|
||||
const { control } = useFormContext();
|
||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const {
|
||||
required,
|
||||
name,
|
||||
defaultChecked = false,
|
||||
shouldUnregister = false,
|
||||
disabled = false,
|
||||
onBlur,
|
||||
onChange,
|
||||
label,
|
||||
FormControlLabelProps,
|
||||
...switchProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
rules={{ required }}
|
||||
name={name}
|
||||
defaultValue={defaultChecked}
|
||||
control={control}
|
||||
shouldUnregister={shouldUnregister}
|
||||
render={({
|
||||
field: {
|
||||
ref,
|
||||
onChange: controllerOnChange,
|
||||
onBlur: controllerOnBlur,
|
||||
value,
|
||||
...field
|
||||
},
|
||||
}) => (
|
||||
<FormControlLabel
|
||||
{...FormControlLabelProps}
|
||||
control={
|
||||
<MuiSwitch
|
||||
{...switchProps}
|
||||
{...field}
|
||||
checked={value}
|
||||
disabled={disabled}
|
||||
onChange={(...args) => {
|
||||
controllerOnChange(...args);
|
||||
onChange?.(...args);
|
||||
}}
|
||||
onBlur={(...args) => {
|
||||
controllerOnBlur();
|
||||
onBlur?.(...args);
|
||||
}}
|
||||
inputRef={(element) => {
|
||||
inputRef.current = element;
|
||||
ref(element);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -14,6 +14,7 @@ type TextFieldProps = {
|
||||
name: string;
|
||||
clickToCopy?: boolean;
|
||||
readOnly?: boolean;
|
||||
'data-test'?: string;
|
||||
} & MuiTextFieldProps;
|
||||
|
||||
const createCopyAdornment = (
|
||||
@@ -44,6 +45,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
disabled = false,
|
||||
onBlur,
|
||||
onChange,
|
||||
'data-test': dataTest,
|
||||
...textFieldProps
|
||||
} = props;
|
||||
|
||||
@@ -65,6 +67,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
<MuiTextField
|
||||
{...textFieldProps}
|
||||
{...field}
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
onChange={(...args) => {
|
||||
controllerOnChange(...args);
|
||||
@@ -82,6 +85,9 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
readOnly,
|
||||
endAdornment: clickToCopy ? createCopyAdornment(inputRef) : null,
|
||||
}}
|
||||
inputProps={{
|
||||
'data-test': dataTest,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@@ -0,0 +1,89 @@
|
||||
import { useTheme } from '@mui/material';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import FirstPageIcon from '@mui/icons-material/FirstPage';
|
||||
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';
|
||||
import LastPageIcon from '@mui/icons-material/LastPage';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
interface TablePaginationActionsProps {
|
||||
count: number;
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
onPageChange: (
|
||||
event: React.MouseEvent<HTMLButtonElement>,
|
||||
newPage: number
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default function TablePaginationActions(
|
||||
props: TablePaginationActionsProps
|
||||
) {
|
||||
const theme = useTheme();
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
|
||||
const handleBackButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
|
||||
const handleNextButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
|
||||
const handleLastPageButtonClick = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: 2.5 }}>
|
||||
<IconButton
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
{theme.direction === 'rtl' ? <LastPageIcon /> : <FirstPageIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
{theme.direction === 'rtl' ? (
|
||||
<KeyboardArrowRight />
|
||||
) : (
|
||||
<KeyboardArrowLeft />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
{theme.direction === 'rtl' ? (
|
||||
<KeyboardArrowLeft />
|
||||
) : (
|
||||
<KeyboardArrowRight />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
{theme.direction === 'rtl' ? <FirstPageIcon /> : <LastPageIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -11,89 +11,132 @@ import Paper from '@mui/material/Paper';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import TableFooter from '@mui/material/TableFooter';
|
||||
|
||||
import DeleteUserButton from 'components/DeleteUserButton/index.ee';
|
||||
import ListLoader from 'components/ListLoader';
|
||||
import useUsers from 'hooks/useUsers';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
import TablePaginationActions from './TablePaginationActions';
|
||||
import { TablePagination } from './style';
|
||||
|
||||
export default function UserList(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { users, loading } = useUsers();
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
const {
|
||||
users,
|
||||
pageInfo,
|
||||
totalCount,
|
||||
loading,
|
||||
} = useUsers(page, rowsPerPage);
|
||||
|
||||
const handleChangePage = (
|
||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||
newPage: number
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setRowsPerPage(+event.target.value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.fullName')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.fullName')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.email')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.email')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.role')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th">
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 700 }}
|
||||
>
|
||||
{formatMessage('userList.role')}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
|
||||
{!loading &&
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
<TableCell component="th" />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading && <ListLoader rowsNumber={3} columnsNumber={2} />}
|
||||
{!loading &&
|
||||
users.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell scope="row">
|
||||
<Typography variant="subtitle2">{user.fullName}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.email}</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">{user.role.name}</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="subtitle2">
|
||||
{user.role.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<TableCell>
|
||||
<Stack direction="row" gap={1} justifyContent="right">
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
to={URLS.USER(user.id)}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<DeleteUserButton userId={user.id} />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
{totalCount && (
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 50, 100]}
|
||||
page={page}
|
||||
count={totalCount}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableFooter>
|
||||
)}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
12
packages/web/src/components/UserList/style.ts
Normal file
12
packages/web/src/components/UserList/style.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { styled } from '@mui/material/styles';
|
||||
import MuiTablePagination, {
|
||||
tablePaginationClasses,
|
||||
} from '@mui/material/TablePagination';
|
||||
|
||||
export const TablePagination = styled(MuiTablePagination)(() => ({
|
||||
[`& .${tablePaginationClasses.selectLabel}, & .${tablePaginationClasses.displayedRows}`]:
|
||||
{
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
},
|
||||
}));
|
@@ -3,12 +3,12 @@ import appConfig from './app';
|
||||
export const CONNECTIONS = '/connections';
|
||||
export const EXECUTIONS = '/executions';
|
||||
export const EXECUTION_PATTERN = '/executions/:executionId';
|
||||
export const EXECUTION = (executionId: string) =>
|
||||
`/executions/${executionId}`;
|
||||
export const EXECUTION = (executionId: string) => `/executions/${executionId}`;
|
||||
|
||||
export const LOGIN = '/login';
|
||||
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
|
||||
export const SSO_LOGIN = (issuer: string) => `${appConfig.apiUrl}/login/saml/${issuer}`;
|
||||
export const SSO_LOGIN = (issuer: string) =>
|
||||
`${appConfig.apiUrl}/login/saml/${issuer}`;
|
||||
export const SIGNUP = '/sign-up';
|
||||
export const FORGOT_PASSWORD = '/forgot-password';
|
||||
export const RESET_PASSWORD = '/reset-password';
|
||||
@@ -17,18 +17,19 @@ export const APPS = '/apps';
|
||||
export const NEW_APP_CONNECTION = '/apps/new';
|
||||
export const APP = (appKey: string) => `/app/${appKey}`;
|
||||
export const APP_PATTERN = '/app/:appKey';
|
||||
export const APP_CONNECTIONS = (appKey: string) =>
|
||||
`/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS = (appKey: string) => `/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||
export const APP_ADD_CONNECTION = (appKey: string, shared = false) =>
|
||||
`/app/${appKey}/connections/add?shared=${shared}`;
|
||||
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (appKey: string, appAuthClientId: string) =>
|
||||
`/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
|
||||
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (
|
||||
appKey: string,
|
||||
appAuthClientId: string
|
||||
) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
|
||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||
export const APP_RECONNECT_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string,
|
||||
appAuthClientId?: string,
|
||||
appAuthClientId?: string
|
||||
) => {
|
||||
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
|
||||
@@ -96,6 +97,8 @@ export const ROLES = `${ADMIN_SETTINGS}/roles`;
|
||||
export const ROLE = (roleId: string) => `${ROLES}/${roleId}`;
|
||||
export const ROLE_PATTERN = `${ROLES}/:roleId`;
|
||||
export const CREATE_ROLE = `${ROLES}/create`;
|
||||
export const USER_INTERFACE = `${ADMIN_SETTINGS}/user-interface`;
|
||||
export const AUTHENTICATION = `${ADMIN_SETTINGS}/authentication`;
|
||||
|
||||
export const DASHBOARD = FLOWS;
|
||||
|
||||
|
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPSERT_SAML_AUTH_PROVIDER = gql`
|
||||
mutation UpsertSamlAuthProvider($input: UpsertSamlAuthProviderInput) {
|
||||
upsertSamlAuthProvider(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
19
packages/web/src/graphql/queries/get-saml-auth-provider.ts
Normal file
19
packages/web/src/graphql/queries/get-saml-auth-provider.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_SAML_AUTH_PROVIDER = gql`
|
||||
query GetSamlAuthProvider {
|
||||
getSamlAuthProvider {
|
||||
name
|
||||
certificate
|
||||
signatureAlgorithm
|
||||
issuer
|
||||
entryPoint
|
||||
firstnameAttributeName
|
||||
surnameAttributeName
|
||||
emailAttributeName
|
||||
roleAttributeName
|
||||
active
|
||||
defaultRoleId
|
||||
}
|
||||
}
|
||||
`;
|
@@ -13,6 +13,7 @@ export const GET_USERS = gql`
|
||||
currentPage
|
||||
totalPages
|
||||
}
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
|
18
packages/web/src/helpers/nestObject.ts
Normal file
18
packages/web/src/helpers/nestObject.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import set from 'lodash/set';
|
||||
|
||||
export default function nestObject<T = IJSONObject>(
|
||||
config: IJSONObject | undefined
|
||||
): Partial<T> {
|
||||
if (!config) return {};
|
||||
const result = {};
|
||||
|
||||
for (const key in config) {
|
||||
if (Object.prototype.hasOwnProperty.call(config, key)) {
|
||||
const value = config[key];
|
||||
set(result, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
20
packages/web/src/hooks/useSamlAuthProvider.ts
Normal file
20
packages/web/src/hooks/useSamlAuthProvider.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
|
||||
import { TSamlAuthProvider } from '@automatisch/types';
|
||||
import { GET_SAML_AUTH_PROVIDER } from 'graphql/queries/get-saml-auth-provider';
|
||||
|
||||
type UseSamlAuthProviderReturn = {
|
||||
provider: TSamlAuthProvider;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export default function useSamlAuthProvider(): UseSamlAuthProviderReturn {
|
||||
const { data, loading } = useQuery(GET_SAML_AUTH_PROVIDER, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
return {
|
||||
provider: data?.getSamlAuthProvider,
|
||||
loading,
|
||||
};
|
||||
}
|
@@ -4,30 +4,37 @@ import { IUser } from '@automatisch/types';
|
||||
import { GET_USERS } from 'graphql/queries/get-users';
|
||||
|
||||
type Edge = {
|
||||
node: IUser
|
||||
}
|
||||
node: IUser;
|
||||
};
|
||||
|
||||
type QueryResponse = {
|
||||
getUsers: {
|
||||
pageInfo: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
edges: Edge[]
|
||||
}
|
||||
}
|
||||
};
|
||||
totalCount: number;
|
||||
edges: Edge[];
|
||||
};
|
||||
};
|
||||
|
||||
export default function useUsers() {
|
||||
const getLimitAndOffset = (page: number, rowsPerPage: number) => ({
|
||||
limit: rowsPerPage,
|
||||
offset: page * rowsPerPage,
|
||||
});
|
||||
|
||||
export default function useUsers(page: number, rowsPerPage: number) {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_USERS, {
|
||||
variables: {
|
||||
limit: 100,
|
||||
offset: 0
|
||||
}
|
||||
variables: getLimitAndOffset(page, rowsPerPage),
|
||||
});
|
||||
const users = data?.getUsers.edges.map(({ node }) => node) || [];
|
||||
const pageInfo = data?.getUsers.pageInfo;
|
||||
const totalCount = data?.getUsers.totalCount;
|
||||
|
||||
return {
|
||||
users,
|
||||
loading
|
||||
pageInfo,
|
||||
totalCount,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@
|
||||
"settingsDrawer.billingAndUsage": "Billing and usage",
|
||||
"adminSettingsDrawer.users": "Users",
|
||||
"adminSettingsDrawer.roles": "Roles",
|
||||
"adminSettingsDrawer.authentication": "Authentication",
|
||||
"adminSettingsDrawer.userInterface": "User Interface",
|
||||
"adminSettingsDrawer.goBack": "Go to the dashboard",
|
||||
"app.connectionCount": "{count} connections",
|
||||
"app.flowCount": "{count} flows",
|
||||
@@ -85,12 +87,14 @@
|
||||
"flowEditor.goBack": "Go back to flows",
|
||||
"executions.title": "Executions",
|
||||
"executions.noExecutions": "There is no execution data point to show.",
|
||||
"execution.id": "Execution ID: {id}",
|
||||
"execution.updatedAt": "updated {datetime}",
|
||||
"execution.test": "Test run",
|
||||
"execution.statusSuccess": "Success",
|
||||
"execution.statusFailure": "Failure",
|
||||
"execution.noDataTitle": "No data",
|
||||
"execution.noDataMessage": "We successfully ran the execution, but there was no new data to process.",
|
||||
"executionStep.id": "ID: {id}",
|
||||
"executionStep.executedAt": "executed {datetime}",
|
||||
"profileSettings.title": "My Profile",
|
||||
"profileSettings.fullName": "Full name",
|
||||
@@ -213,5 +217,26 @@
|
||||
"permissionSettings.cancel": "Cancel",
|
||||
"permissionSettings.apply": "Apply",
|
||||
"permissionSettings.title": "Conditions",
|
||||
"appAuthClientsDialog.title": "Choose your authentication client"
|
||||
"appAuthClientsDialog.title": "Choose your authentication client",
|
||||
"userInterfacePage.title": "User Interface",
|
||||
"userInterfacePage.successfullyUpdated": "User interface has been updated.",
|
||||
"userInterfacePage.mainColor": "Primary main color",
|
||||
"userInterfacePage.darkColor": "Primary dark color",
|
||||
"userInterfacePage.lightColor": "Primary light color",
|
||||
"userInterfacePage.svgData": "Logo SVG code",
|
||||
"userInterfacePage.submit": "Update",
|
||||
"authenticationPage.title": "Single Sign-On with SAML",
|
||||
"authenticationForm.active": "Active",
|
||||
"authenticationForm.name": "Name",
|
||||
"authenticationForm.certificate": "Certificate",
|
||||
"authenticationForm.signatureAlgorithm": "Signature algorithm",
|
||||
"authenticationForm.issuer": "Issuer",
|
||||
"authenticationForm.entryPoint": "Entry point",
|
||||
"authenticationForm.firstnameAttributeName": "Firstname attribute name",
|
||||
"authenticationForm.surnameAttributeName": "Surname attribute name",
|
||||
"authenticationForm.emailAttributeName": "Email attribute name",
|
||||
"authenticationForm.roleAttributeName": "Role attribute name",
|
||||
"authenticationForm.defaultRole": "Default role",
|
||||
"authenticationForm.successfullySaved": "The provider has been saved.",
|
||||
"authenticationForm.save": "Save"
|
||||
}
|
||||
|
215
packages/web/src/pages/Authentication/index.tsx
Normal file
215
packages/web/src/pages/Authentication/index.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import * as React from 'react';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { IRole } from '@automatisch/types';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { TSamlAuthProvider } from '@automatisch/types';
|
||||
import { useMutation } from '@apollo/client';
|
||||
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import Switch from 'components/Switch';
|
||||
|
||||
import { UPSERT_SAML_AUTH_PROVIDER } from 'graphql/mutations/upsert-saml-auth-provider';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import useSamlAuthProvider from 'hooks/useSamlAuthProvider';
|
||||
|
||||
const defaultValues = {
|
||||
active: false,
|
||||
name: '',
|
||||
certificate: '',
|
||||
signatureAlgorithm: 'sha1',
|
||||
issuer: '',
|
||||
entryPoint: '',
|
||||
firstnameAttributeName: '',
|
||||
surnameAttributeName: '',
|
||||
emailAttributeName: '',
|
||||
roleAttributeName: '',
|
||||
defaultRoleId: '',
|
||||
};
|
||||
|
||||
function generateRoleOptions(roles: IRole[]) {
|
||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||
}
|
||||
|
||||
function AuthenticationPage() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { roles, loading: rolesLoading } = useRoles();
|
||||
const { provider, loading: providerLoading } = useSamlAuthProvider();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [upsertSamlAuthProvider, { loading }] = useMutation(
|
||||
UPSERT_SAML_AUTH_PROVIDER
|
||||
);
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
providerDataToUpdate: Partial<TSamlAuthProvider>
|
||||
) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
certificate,
|
||||
signatureAlgorithm,
|
||||
issuer,
|
||||
entryPoint,
|
||||
firstnameAttributeName,
|
||||
surnameAttributeName,
|
||||
emailAttributeName,
|
||||
roleAttributeName,
|
||||
active,
|
||||
defaultRoleId,
|
||||
} = providerDataToUpdate;
|
||||
|
||||
await upsertSamlAuthProvider({
|
||||
variables: {
|
||||
input: {
|
||||
name,
|
||||
certificate,
|
||||
signatureAlgorithm,
|
||||
issuer,
|
||||
entryPoint,
|
||||
firstnameAttributeName,
|
||||
surnameAttributeName,
|
||||
emailAttributeName,
|
||||
roleAttributeName,
|
||||
active,
|
||||
defaultRoleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while saving!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid container item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('authenticationPage.title')}</PageTitle>
|
||||
</Grid>
|
||||
<Grid item xs={12} sx={{ pt: 5, pb: 5 }}>
|
||||
{!providerLoading && (
|
||||
<Form
|
||||
defaultValues={provider || defaultValues}
|
||||
onSubmit={handleProviderUpdate}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
<Switch
|
||||
name="active"
|
||||
label={formatMessage('authenticationForm.active')}
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="name"
|
||||
label={formatMessage('authenticationForm.name')}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="certificate"
|
||||
label={formatMessage('authenticationForm.certificate')}
|
||||
fullWidth
|
||||
multiline
|
||||
/>
|
||||
<ControlledAutocomplete
|
||||
name="signatureAlgorithm"
|
||||
fullWidth
|
||||
disablePortal
|
||||
disableClearable={true}
|
||||
options={[
|
||||
{ label: 'SHA1', value: 'sha1' },
|
||||
{ label: 'SHA256', value: 'sha256' },
|
||||
{ label: 'SHA512', value: 'sha512' },
|
||||
]}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
label={formatMessage(
|
||||
'authenticationForm.signatureAlgorithm'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="issuer"
|
||||
label={formatMessage('authenticationForm.issuer')}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="entryPoint"
|
||||
label={formatMessage('authenticationForm.entryPoint')}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="firstnameAttributeName"
|
||||
label={formatMessage(
|
||||
'authenticationForm.firstnameAttributeName'
|
||||
)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="surnameAttributeName"
|
||||
label={formatMessage(
|
||||
'authenticationForm.surnameAttributeName'
|
||||
)}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="emailAttributeName"
|
||||
label={formatMessage('authenticationForm.emailAttributeName')}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
required={true}
|
||||
name="roleAttributeName"
|
||||
label={formatMessage('authenticationForm.roleAttributeName')}
|
||||
fullWidth
|
||||
/>
|
||||
<ControlledAutocomplete
|
||||
name="defaultRoleId"
|
||||
fullWidth
|
||||
disablePortal
|
||||
disableClearable={true}
|
||||
options={generateRoleOptions(roles)}
|
||||
renderInput={(params) => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
label={formatMessage('authenticationForm.defaultRole')}
|
||||
/>
|
||||
)}
|
||||
loading={rolesLoading}
|
||||
/>
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2 }}
|
||||
loading={loading}
|
||||
>
|
||||
{formatMessage('authenticationForm.save')}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Form>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthenticationPage;
|
@@ -1,6 +1,5 @@
|
||||
import { useMutation } from '@apollo/client';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import * as React from 'react';
|
||||
@@ -10,6 +9,7 @@ import { useSnackbar } from 'notistack';
|
||||
|
||||
import Form from 'components/Form';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import TextField from 'components/TextField';
|
||||
import * as URLS from 'config/urls';
|
||||
import { CREATE_ROLE } from 'graphql/mutations/create-role.ee';
|
||||
@@ -53,7 +53,7 @@ export default function CreateRole(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('createRolePage.title')}</PageTitle>
|
||||
</Grid>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
@@ -14,6 +13,7 @@ import * as URLS from 'config/urls';
|
||||
import Can from 'components/Can';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import Form from 'components/Form';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import TextField from 'components/TextField';
|
||||
@@ -57,7 +57,7 @@ export default function CreateUser(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('createUserPage.title')}</PageTitle>
|
||||
</Grid>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { useMutation } from '@apollo/client';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
@@ -10,6 +9,7 @@ import { useSnackbar } from 'notistack';
|
||||
|
||||
import Form from 'components/Form';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import PermissionCatalogField from 'components/PermissionCatalogField/index.ee';
|
||||
import TextField from 'components/TextField';
|
||||
import * as URLS from 'config/urls';
|
||||
@@ -65,7 +65,7 @@ export default function EditRole(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('editRolePage.title')}</PageTitle>
|
||||
</Grid>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Container from '@mui/material/Container';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MuiTextField from '@mui/material/TextField';
|
||||
@@ -16,6 +15,7 @@ import * as URLS from 'config/urls';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useRoles from 'hooks/useRoles.ee';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import Form from 'components/Form';
|
||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||
import TextField from 'components/TextField';
|
||||
@@ -65,7 +65,7 @@ export default function EditUser(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={9} md={8} lg={6}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('editUserPage.title')}</PageTitle>
|
||||
</Grid>
|
||||
|
135
packages/web/src/pages/UserInterface/index.tsx
Normal file
135
packages/web/src/pages/UserInterface/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as React from 'react';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import Container from 'components/Container';
|
||||
import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import ColorInput from 'components/ColorInput';
|
||||
import nestObject from 'helpers/nestObject';
|
||||
import { Skeleton } from '@mui/material';
|
||||
|
||||
type UserInterface = {
|
||||
palette: {
|
||||
primary: {
|
||||
dark: string;
|
||||
light: string;
|
||||
main: string;
|
||||
};
|
||||
};
|
||||
logo: {
|
||||
svgData: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function UserInterface(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG, {
|
||||
refetchQueries: ['GetConfig'],
|
||||
});
|
||||
const { config, loading: configLoading } = useConfig([
|
||||
'palette.primary.main',
|
||||
'palette.primary.light',
|
||||
'palette.primary.dark',
|
||||
'logo.svgData',
|
||||
]);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleUserInterfaceUpdate = async (uiData: Partial<UserInterface>) => {
|
||||
try {
|
||||
await updateConfig({
|
||||
variables: {
|
||||
input: {
|
||||
'palette.primary.main': uiData?.palette?.primary.main,
|
||||
'palette.primary.dark': uiData?.palette?.primary.dark,
|
||||
'palette.primary.light': uiData?.palette?.primary.light,
|
||||
'logo.svgData': uiData?.logo?.svgData,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('userInterfacePage.successfullyUpdated'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error('Failed while updating!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid container item xs={12} sm={10} md={9}>
|
||||
<Grid item xs={12} sx={{ mb: [2, 5] }}>
|
||||
<PageTitle>{formatMessage('userInterfacePage.title')}</PageTitle>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||
{configLoading && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<Skeleton variant="rounded" height={55} />
|
||||
<Skeleton variant="rounded" height={55} />
|
||||
<Skeleton variant="rounded" height={55} />
|
||||
<Skeleton variant="rounded" height={85} />
|
||||
<Skeleton variant="rounded" height={45} />
|
||||
</Stack>
|
||||
)}
|
||||
{!configLoading && (
|
||||
<Form
|
||||
onSubmit={handleUserInterfaceUpdate}
|
||||
defaultValues={nestObject<UserInterface>(config)}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
<ColorInput
|
||||
name="palette.primary.main"
|
||||
label={formatMessage('userInterfacePage.mainColor')}
|
||||
fullWidth
|
||||
data-test="primary-main-color-input"
|
||||
/>
|
||||
|
||||
<ColorInput
|
||||
name="palette.primary.dark"
|
||||
label={formatMessage('userInterfacePage.darkColor')}
|
||||
fullWidth
|
||||
data-test="primary-dark-color-input"
|
||||
/>
|
||||
|
||||
<ColorInput
|
||||
name="palette.primary.light"
|
||||
label={formatMessage('userInterfacePage.lightColor')}
|
||||
fullWidth
|
||||
data-test="primary-light-color-input"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name="logo.svgData"
|
||||
label={formatMessage('userInterfacePage.svgData')}
|
||||
multiline
|
||||
fullWidth
|
||||
data-test="logo-svg-data-text-field"
|
||||
/>
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ boxShadow: 2 }}
|
||||
loading={loading}
|
||||
data-test="update-button"
|
||||
>
|
||||
{formatMessage('userInterfacePage.submit')}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Form>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
413
yarn.lock
413
yarn.lock
@@ -1469,37 +1469,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
|
||||
integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==
|
||||
|
||||
"@cypress/request@^2.88.10":
|
||||
version "2.88.10"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
|
||||
integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.8.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.6"
|
||||
extend "~3.0.2"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.2"
|
||||
http-signature "~1.3.6"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.19"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.2"
|
||||
safe-buffer "^5.1.2"
|
||||
tough-cookie "~2.5.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^8.3.2"
|
||||
|
||||
"@cypress/xvfb@^1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a"
|
||||
integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
"@ctrl/tinycolor@^3.6.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz#53fa5fe9c34faee89469e48f91d51a3766108bc8"
|
||||
integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
|
||||
|
||||
"@dabh/diagnostics@^2.0.2":
|
||||
version "2.0.2"
|
||||
@@ -2969,9 +2942,9 @@
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@node-saml/node-saml@^4.0.4":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@node-saml/node-saml/-/node-saml-4.0.4.tgz#472a6b17021a0c9d8261964bf6e1dd686ae2d515"
|
||||
integrity sha512-oybUBWBYVsHGckQxzyzlpRM4E2iuW3I2Ok/J9SwlotdmjvmZxSo6Ub74D9wltG8C9daJZYI57uy+1UK4FtcGXA==
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@node-saml/node-saml/-/node-saml-4.0.5.tgz#039e387095b54639b06df62b1b4a6d8941c6d907"
|
||||
integrity sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw==
|
||||
dependencies:
|
||||
"@types/debug" "^4.1.7"
|
||||
"@types/passport" "^1.0.11"
|
||||
@@ -3380,6 +3353,16 @@
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^11.2.0"
|
||||
|
||||
"@playwright/test@^1.36.2":
|
||||
version "1.36.2"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.36.2.tgz#9edd68a02b0929c5d78d9479a654ceb981dfb592"
|
||||
integrity sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
playwright-core "1.36.2"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz#df0d0d855fc527db48aac93c218a0bf4ada41f99"
|
||||
@@ -4154,11 +4137,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.42.tgz#2f021733232c2130c26f9eabbdd3bfd881774733"
|
||||
integrity sha512-aI3/oo5DzyiI5R/xAhxxRzfZlWlsbbqdgxfTPkqu/Zt+23GXiJvMCyPJT4+xKSXOnLqoL8jJYMLTwvK2M3a5hw==
|
||||
|
||||
"@types/node@^14.14.31":
|
||||
version "14.18.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.31.tgz#4b873dea3122e71af4f77e65ec5841397ff254d3"
|
||||
integrity sha512-vQAnaReSQkEDa8uwAyQby8bYGKu84R/deEc6mg5T8fX6gzCn8QW6rziSgsti1fNvsrswKUKPnVTi7uoB+u62Mw==
|
||||
|
||||
"@types/node@^15.6.1":
|
||||
version "15.14.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa"
|
||||
@@ -4360,16 +4338,11 @@
|
||||
dependencies:
|
||||
"@types/sinonjs__fake-timers" "*"
|
||||
|
||||
"@types/sinonjs__fake-timers@*", "@types/sinonjs__fake-timers@8.1.1":
|
||||
"@types/sinonjs__fake-timers@*":
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
|
||||
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
|
||||
|
||||
"@types/sizzle@^2.3.2":
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
|
||||
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
|
||||
|
||||
"@types/sockjs@^0.3.33":
|
||||
version "0.3.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f"
|
||||
@@ -4477,13 +4450,6 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
|
||||
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/yup@0.29.11":
|
||||
version "0.29.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e"
|
||||
@@ -5270,11 +5236,6 @@ ansi-align@^3.0.0:
|
||||
dependencies:
|
||||
string-width "^4.1.0"
|
||||
|
||||
ansi-colors@^4.1.1:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"
|
||||
integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==
|
||||
|
||||
ansi-escapes@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
|
||||
@@ -5364,11 +5325,6 @@ aproba@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
|
||||
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
|
||||
|
||||
arch@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
|
||||
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
|
||||
|
||||
are-we-there-yet@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
|
||||
@@ -5546,11 +5502,6 @@ async@^2.6.2:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
|
||||
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
|
||||
|
||||
async@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
|
||||
@@ -5952,12 +5903,7 @@ bl@^4.0.3, bl@^4.1.0:
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
blob-util@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
||||
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
||||
|
||||
bluebird@^3.5.5, bluebird@^3.7.2:
|
||||
bluebird@^3.5.5:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
@@ -6082,11 +6028,6 @@ bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64 "^0.4.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
|
||||
@@ -6116,7 +6057,7 @@ buffer@4.9.2:
|
||||
ieee754 "^1.1.4"
|
||||
isarray "^1.0.0"
|
||||
|
||||
buffer@^5.5.0, buffer@^5.6.0:
|
||||
buffer@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
@@ -6232,11 +6173,6 @@ cacheable-request@^6.0.0:
|
||||
normalize-url "^4.1.0"
|
||||
responselike "^1.0.2"
|
||||
|
||||
cachedir@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
|
||||
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
|
||||
|
||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
||||
@@ -6376,11 +6312,6 @@ charenc@0.0.2:
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
|
||||
|
||||
check-more-types@^2.24.0:
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
|
||||
integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==
|
||||
|
||||
check-types@^11.1.1:
|
||||
version "11.1.2"
|
||||
resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
|
||||
@@ -6501,15 +6432,6 @@ cli-spinners@^2.5.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
|
||||
integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
|
||||
|
||||
cli-table3@~0.6.1:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2"
|
||||
integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
optionalDependencies:
|
||||
"@colors/colors" "1.5.0"
|
||||
|
||||
cli-table@^0.3.1:
|
||||
version "0.3.11"
|
||||
resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.11.tgz#ac69cdecbe81dccdba4889b9a18b7da312a9d3ee"
|
||||
@@ -6692,7 +6614,7 @@ colord@^2.9.1:
|
||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
|
||||
integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==
|
||||
|
||||
colorette@2.0.19, colorette@^2.0.10, colorette@^2.0.16:
|
||||
colorette@2.0.19, colorette@^2.0.10:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
|
||||
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
|
||||
@@ -6740,11 +6662,6 @@ commander@^2.20.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
|
||||
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
|
||||
|
||||
commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
@@ -7401,54 +7318,6 @@ currently-unhandled@^0.4.1:
|
||||
dependencies:
|
||||
array-find-index "^1.0.1"
|
||||
|
||||
cypress@^10.9.0:
|
||||
version "10.9.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.9.0.tgz#273a61a6304766f9d6423e5ac8d4a9a11ed8b485"
|
||||
integrity sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/node" "^14.14.31"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
arch "^2.2.0"
|
||||
blob-util "^2.0.2"
|
||||
bluebird "^3.7.2"
|
||||
buffer "^5.6.0"
|
||||
cachedir "^2.3.0"
|
||||
chalk "^4.1.0"
|
||||
check-more-types "^2.24.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-table3 "~0.6.1"
|
||||
commander "^5.1.0"
|
||||
common-tags "^1.8.0"
|
||||
dayjs "^1.10.4"
|
||||
debug "^4.3.2"
|
||||
enquirer "^2.3.6"
|
||||
eventemitter2 "6.4.7"
|
||||
execa "4.1.0"
|
||||
executable "^4.1.1"
|
||||
extract-zip "2.0.1"
|
||||
figures "^3.2.0"
|
||||
fs-extra "^9.1.0"
|
||||
getos "^3.2.1"
|
||||
is-ci "^3.0.0"
|
||||
is-installed-globally "~0.4.0"
|
||||
lazy-ass "^1.6.0"
|
||||
listr2 "^3.8.3"
|
||||
lodash "^4.17.21"
|
||||
log-symbols "^4.0.0"
|
||||
minimist "^1.2.6"
|
||||
ospath "^1.2.2"
|
||||
pretty-bytes "^5.6.0"
|
||||
proxy-from-env "1.0.0"
|
||||
request-progress "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
untildify "^4.0.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
damerau-levenshtein@^1.0.7:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
@@ -7502,11 +7371,6 @@ dateformat@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
|
||||
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
|
||||
|
||||
dayjs@^1.10.4:
|
||||
version "1.11.5"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
|
||||
integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
|
||||
|
||||
db-errors@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/db-errors/-/db-errors-0.2.3.tgz#a6a38952e00b20e790f2695a6446b3c65497ffa2"
|
||||
@@ -7526,7 +7390,7 @@ debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, de
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.1.0, debug@^3.1.1, debug@^3.2.7:
|
||||
debug@^3.1.1, debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
@@ -7961,6 +7825,11 @@ dotenv@^10.0.0:
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||
|
||||
dotenv@^16.3.1:
|
||||
version "16.3.1"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
|
||||
|
||||
duplexer3@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
|
||||
@@ -8072,13 +7941,6 @@ enhanced-resolve@^5.10.0:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enquirer@^2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
|
||||
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
|
||||
dependencies:
|
||||
ansi-colors "^4.1.1"
|
||||
|
||||
entities@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
|
||||
@@ -8799,11 +8661,6 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
eventemitter2@6.4.7:
|
||||
version "6.4.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
|
||||
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
|
||||
|
||||
eventemitter3@^4.0.0, eventemitter3@^4.0.4:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||
@@ -8819,7 +8676,20 @@ events@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
|
||||
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
|
||||
|
||||
execa@4.1.0, execa@^4.1.0:
|
||||
execa@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
|
||||
integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^3.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
|
||||
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
|
||||
@@ -8834,19 +8704,6 @@ execa@4.1.0, execa@^4.1.0:
|
||||
signal-exit "^3.0.2"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
execa@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
|
||||
integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^3.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
@@ -8862,13 +8719,6 @@ execa@^5.0.0:
|
||||
signal-exit "^3.0.3"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
executable@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
|
||||
integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
|
||||
dependencies:
|
||||
pify "^2.2.0"
|
||||
|
||||
exit@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
|
||||
@@ -8988,17 +8838,6 @@ external-editor@^3.0.3:
|
||||
iconv-lite "^0.4.24"
|
||||
tmp "^0.0.33"
|
||||
|
||||
extract-zip@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
|
||||
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
get-stream "^5.1.0"
|
||||
yauzl "^2.10.0"
|
||||
optionalDependencies:
|
||||
"@types/yauzl" "^2.9.1"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
@@ -9099,13 +8938,6 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "2.1.1"
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
|
||||
integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fecha@^4.2.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce"
|
||||
@@ -9400,7 +9232,7 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
fsevents@^2.3.2, fsevents@~2.3.2:
|
||||
fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
@@ -9554,13 +9386,6 @@ getopts@2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4"
|
||||
integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==
|
||||
|
||||
getos@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
|
||||
integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==
|
||||
dependencies:
|
||||
async "^3.2.0"
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
@@ -10133,15 +9958,6 @@ http-signature@~1.2.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
http-signature@~1.3.6:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9"
|
||||
integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
jsprim "^2.0.2"
|
||||
sshpk "^1.14.1"
|
||||
|
||||
https-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
@@ -10521,13 +10337,6 @@ is-ci@^2.0.0:
|
||||
dependencies:
|
||||
ci-info "^2.0.0"
|
||||
|
||||
is-ci@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867"
|
||||
integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==
|
||||
dependencies:
|
||||
ci-info "^3.2.0"
|
||||
|
||||
is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
|
||||
@@ -10598,7 +10407,7 @@ is-hotkey@^0.1.6:
|
||||
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.8.tgz#6b1f4b2d0e5639934e20c05ed24d623a21d36d25"
|
||||
integrity sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==
|
||||
|
||||
is-installed-globally@^0.4.0, is-installed-globally@~0.4.0:
|
||||
is-installed-globally@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
|
||||
integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
|
||||
@@ -11528,16 +11337,6 @@ jsprim@^1.2.2:
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
jsprim@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d"
|
||||
integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==
|
||||
dependencies:
|
||||
assert-plus "1.0.0"
|
||||
extsprintf "1.3.0"
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b"
|
||||
@@ -11644,11 +11443,6 @@ latest-version@^5.1.0:
|
||||
dependencies:
|
||||
package-json "^6.3.0"
|
||||
|
||||
lazy-ass@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
|
||||
integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==
|
||||
|
||||
lerna@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e"
|
||||
@@ -11725,20 +11519,6 @@ lines-and-columns@^1.1.6:
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
|
||||
|
||||
listr2@^3.8.3:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e"
|
||||
integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==
|
||||
dependencies:
|
||||
cli-truncate "^2.1.0"
|
||||
colorette "^2.0.16"
|
||||
log-update "^4.0.0"
|
||||
p-map "^4.0.0"
|
||||
rfdc "^1.3.0"
|
||||
rxjs "^7.5.1"
|
||||
through "^2.3.8"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
load-json-file@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
|
||||
@@ -11903,11 +11683,6 @@ lodash.merge@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.once@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
||||
|
||||
lodash.set@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||
@@ -11951,16 +11726,6 @@ log-symbols@^4.0.0, log-symbols@^4.1.0:
|
||||
chalk "^4.1.0"
|
||||
is-unicode-supported "^0.1.0"
|
||||
|
||||
log-update@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
|
||||
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
|
||||
dependencies:
|
||||
ansi-escapes "^4.3.0"
|
||||
cli-cursor "^3.1.0"
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
logform@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/logform/-/logform-2.3.2.tgz#68babe6a74ab09a1fd15a9b1e6cbc7713d41cb5b"
|
||||
@@ -12615,6 +12380,13 @@ msgpackr@^1.6.2:
|
||||
optionalDependencies:
|
||||
msgpackr-extract "^2.1.2"
|
||||
|
||||
mui-color-input@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mui-color-input/-/mui-color-input-2.0.0.tgz#49c8df63d3d18f1a1817572c0efc15bd970b35a2"
|
||||
integrity sha512-Xw6OGsZVbtlZEAUVgJ08Lyv4u0YDQH+aTMJhhWm2fRin+1T+0IrVFyBtbSjJjrH4aBkkQPMCm75//7qO9zncLw==
|
||||
dependencies:
|
||||
"@ctrl/tinycolor" "^3.6.0"
|
||||
|
||||
multer@1.4.5-lts.1:
|
||||
version "1.4.5-lts.1"
|
||||
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac"
|
||||
@@ -13384,11 +13156,6 @@ osenv@^0.1.4:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
ospath@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
|
||||
integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==
|
||||
|
||||
p-cancelable@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
|
||||
@@ -13771,11 +13538,6 @@ pause@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
|
||||
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
|
||||
|
||||
pend@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
@@ -13852,7 +13614,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
pify@^2.2.0, pify@^2.3.0:
|
||||
pify@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
|
||||
@@ -13928,6 +13690,11 @@ pkg-up@^3.1.0:
|
||||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
playwright-core@1.36.2:
|
||||
version "1.36.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.2.tgz#32382f2d96764c24c65a86ea336cf79721c2e50e"
|
||||
integrity sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==
|
||||
|
||||
plur@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/plur/-/plur-4.0.0.tgz#729aedb08f452645fe8c58ef115bf16b0a73ef84"
|
||||
@@ -14549,7 +14316,7 @@ prettier@^2.5.1:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
|
||||
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1, pretty-bytes@^5.6.0:
|
||||
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
@@ -14690,11 +14457,6 @@ proxy-addr@~2.0.7:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-from-env@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
|
||||
integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==
|
||||
|
||||
psl@^1.1.28, psl@^1.1.33:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||
@@ -15416,13 +15178,6 @@ replace-ext@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
|
||||
integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
|
||||
|
||||
request-progress@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
|
||||
integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==
|
||||
dependencies:
|
||||
throttleit "^1.0.0"
|
||||
|
||||
request@^2.88.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
@@ -15562,11 +15317,6 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rfdc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
|
||||
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
|
||||
|
||||
rimraf@^2.6.1, rimraf@^2.6.3:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
@@ -15631,13 +15381,6 @@ rxjs@^7.2.0:
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
rxjs@^7.5.1:
|
||||
version "7.5.7"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39"
|
||||
integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
@@ -16077,15 +15820,6 @@ slice-ansi@^3.0.0:
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
slice-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
|
||||
integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
astral-regex "^2.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
|
||||
slide@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
|
||||
@@ -16302,7 +16036,7 @@ sprintf-js@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
sshpk@^1.14.1, sshpk@^1.7.0:
|
||||
sshpk@^1.7.0:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
|
||||
integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
|
||||
@@ -16919,11 +16653,6 @@ throat@^6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
|
||||
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
|
||||
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==
|
||||
|
||||
through2@^2.0.0, through2@^2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
@@ -16939,7 +16668,7 @@ through2@^4.0.0:
|
||||
dependencies:
|
||||
readable-stream "3"
|
||||
|
||||
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8:
|
||||
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
@@ -16988,13 +16717,6 @@ tmp@^0.1.0:
|
||||
dependencies:
|
||||
rimraf "^2.6.3"
|
||||
|
||||
tmp@~0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
|
||||
tmpl@1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
|
||||
@@ -18154,15 +17876,6 @@ wrap-ansi@^2.0.0:
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
|
||||
wrap-ansi@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
|
||||
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
@@ -18386,14 +18099,6 @@ yarn@^1.22.17:
|
||||
resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.17.tgz#bf910747d22497b573131f7341c0e1d15c74036c"
|
||||
integrity sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||
integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==
|
||||
dependencies:
|
||||
buffer-crc32 "~0.2.3"
|
||||
fd-slicer "~1.1.0"
|
||||
|
||||
yeoman-environment@^3.9.1:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-3.9.1.tgz#21912bdee4b1d302a5c25a7d31338fa092ea7116"
|
||||
|
Reference in New Issue
Block a user