Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c1d2c47ec | ||
![]() |
84ecf105c2 | ||
![]() |
c07a02ef31 | ||
![]() |
3c44f55f19 | ||
![]() |
5b7b8c934f | ||
![]() |
b70223e824 | ||
![]() |
9900bbbc8d | ||
![]() |
fc7f1ddd69 | ||
![]() |
991b2f4bf7 | ||
![]() |
bb251b16a9 | ||
![]() |
1b34a48a61 | ||
![]() |
8c83b715fe | ||
![]() |
c122708b0b | ||
![]() |
258d920ff2 | ||
![]() |
6e5c0cc0c7 | ||
![]() |
9dc82290b5 | ||
![]() |
bb73f90374 | ||
![]() |
8a0720b0e3 | ||
![]() |
88468c4f89 | ||
![]() |
d19a45592f | ||
![]() |
21a921d25d | ||
![]() |
3b2946aac5 | ||
![]() |
196d555e8c | ||
![]() |
28f39b5c7e | ||
![]() |
ec8ac17f4a | ||
![]() |
c45573349a | ||
![]() |
d36c9d43f6 | ||
![]() |
b06c744392 | ||
![]() |
c413ab030b |
@@ -20,6 +20,7 @@
|
||||
"db:migrate": "node ./bin/database/convert-migrations.js && knex migrate:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.12.13",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
|
@@ -0,0 +1,92 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create record',
|
||||
key: 'createRecord',
|
||||
description: 'Creates a new record with fields that automatically populate.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Base',
|
||||
key: 'baseId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: 'Base in which to create the record.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listBases',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
key: 'tableId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
dependsOn: ['parameters.baseId'],
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTables',
|
||||
},
|
||||
{
|
||||
name: 'parameters.baseId',
|
||||
value: '{parameters.baseId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
additionalFields: {
|
||||
type: 'query',
|
||||
name: 'getDynamicFields',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listFields',
|
||||
},
|
||||
{
|
||||
name: 'parameters.baseId',
|
||||
value: '{parameters.baseId}',
|
||||
},
|
||||
{
|
||||
name: 'parameters.tableId',
|
||||
value: '{parameters.tableId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const { baseId, tableId, ...rest } = $.step.parameters;
|
||||
|
||||
const fields = Object.entries(rest).reduce((result, [key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
result[key] = value.map((item) => item.value);
|
||||
} else if (value !== '') {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const body = {
|
||||
typecast: true,
|
||||
fields,
|
||||
};
|
||||
|
||||
const { data } = await $.http.post(`/v0/${baseId}/${tableId}`, body);
|
||||
|
||||
$.setActionItem({
|
||||
raw: data,
|
||||
});
|
||||
},
|
||||
});
|
174
packages/backend/src/apps/airtable/actions/find-record/index.js
Normal file
174
packages/backend/src/apps/airtable/actions/find-record/index.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Find record',
|
||||
key: 'findRecord',
|
||||
description:
|
||||
"Finds a record using simple field search or use Airtable's formula syntax to find a matching record.",
|
||||
arguments: [
|
||||
{
|
||||
label: 'Base',
|
||||
key: 'baseId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: 'Base in which to create the record.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listBases',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
key: 'tableId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
dependsOn: ['parameters.baseId'],
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTables',
|
||||
},
|
||||
{
|
||||
name: 'parameters.baseId',
|
||||
value: '{parameters.baseId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Search by field',
|
||||
key: 'tableField',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
dependsOn: ['parameters.baseId', 'parameters.tableId'],
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTableFields',
|
||||
},
|
||||
{
|
||||
name: 'parameters.baseId',
|
||||
value: '{parameters.baseId}',
|
||||
},
|
||||
{
|
||||
name: 'parameters.tableId',
|
||||
value: '{parameters.tableId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Search Value',
|
||||
key: 'searchValue',
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
description:
|
||||
'The value of unique identifier for the record. For date values, please use the ISO format (e.g., "YYYY-MM-DD").',
|
||||
},
|
||||
{
|
||||
label: 'Search for exact match?',
|
||||
key: 'exactMatch',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'Yes', value: 'true' },
|
||||
{ label: 'No', value: 'false' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Search Formula',
|
||||
key: 'searchFormula',
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
description:
|
||||
'Instead, you have the option to use an Airtable search formula for locating records according to sophisticated criteria and across various fields.',
|
||||
},
|
||||
{
|
||||
label: 'Limit to View',
|
||||
key: 'limitToView',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
dependsOn: ['parameters.baseId', 'parameters.tableId'],
|
||||
description:
|
||||
'You have the choice to restrict the search to a particular view ID if desired.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listTableViews',
|
||||
},
|
||||
{
|
||||
name: 'parameters.baseId',
|
||||
value: '{parameters.baseId}',
|
||||
},
|
||||
{
|
||||
name: 'parameters.tableId',
|
||||
value: '{parameters.tableId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const {
|
||||
baseId,
|
||||
tableId,
|
||||
tableField,
|
||||
searchValue,
|
||||
exactMatch,
|
||||
searchFormula,
|
||||
limitToView,
|
||||
} = $.step.parameters;
|
||||
|
||||
let filterByFormula;
|
||||
|
||||
if (tableField && searchValue) {
|
||||
filterByFormula =
|
||||
exactMatch === 'true'
|
||||
? `{${tableField}} = '${searchValue}'`
|
||||
: `LOWER({${tableField}}) = LOWER('${searchValue}')`;
|
||||
} else {
|
||||
filterByFormula = searchFormula;
|
||||
}
|
||||
|
||||
const body = new URLSearchParams({
|
||||
filterByFormula,
|
||||
view: limitToView,
|
||||
});
|
||||
|
||||
const { data } = await $.http.post(
|
||||
`/v0/${baseId}/${tableId}/listRecords`,
|
||||
body
|
||||
);
|
||||
|
||||
$.setActionItem({
|
||||
raw: data,
|
||||
});
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/airtable/actions/index.js
Normal file
4
packages/backend/src/apps/airtable/actions/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import createRecord from './create-record/index.js';
|
||||
import findRecord from './find-record/index.js';
|
||||
|
||||
export default [createRecord, findRecord];
|
9
packages/backend/src/apps/airtable/assets/favicon.svg
Normal file
9
packages/backend/src/apps/airtable/assets/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256px" height="215px" viewBox="0 0 256 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695" fill="#FFBF00"></path>
|
||||
<path d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863" fill="#26B5F8"></path>
|
||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill="#ED3049"></path>
|
||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill-opacity="0.25" fill="#000000"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
38
packages/backend/src/apps/airtable/auth/generate-auth-url.js
Normal file
38
packages/backend/src/apps/airtable/auth/generate-auth-url.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import crypto from 'crypto';
|
||||
import { URLSearchParams } from 'url';
|
||||
import authScope from '../common/auth-scope.js';
|
||||
|
||||
export default async function generateAuthUrl($) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value;
|
||||
const state = crypto.randomBytes(100).toString('base64url');
|
||||
const codeVerifier = crypto.randomBytes(96).toString('base64url');
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: authScope.join(' '),
|
||||
state,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
});
|
||||
|
||||
const url = `https://airtable.com/oauth2/v1/authorize?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
originalCodeChallenge: codeChallenge,
|
||||
originalState: state,
|
||||
codeVerifier,
|
||||
});
|
||||
}
|
48
packages/backend/src/apps/airtable/auth/index.js
Normal file
48
packages/backend/src/apps/airtable/auth/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import generateAuthUrl from './generate-auth-url.js';
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
import refreshToken from './refresh-token.js';
|
||||
import isStillVerified from './is-still-verified.js';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/airtable/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input a redirect URL in Airtable, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import getCurrentUser from '../common/get-current-user.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
const currentUser = await getCurrentUser($);
|
||||
return !!currentUser.id;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
40
packages/backend/src/apps/airtable/auth/refresh-token.js
Normal file
40
packages/backend/src/apps/airtable/auth/refresh-token.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
|
||||
import authScope from '../common/auth-scope.js';
|
||||
|
||||
const refreshToken = async ($) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: $.auth.data.refreshToken,
|
||||
});
|
||||
|
||||
const basicAuthToken = Buffer.from(
|
||||
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||
).toString('base64');
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'https://airtable.com/oauth2/v1/token',
|
||||
params.toString(),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
},
|
||||
additionalProperties: {
|
||||
skipAddingAuthHeader: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresIn: data.expires_in,
|
||||
refreshExpiresIn: data.refresh_expires_in,
|
||||
scope: authScope.join(' '),
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
@@ -0,0 +1,56 @@
|
||||
import getCurrentUser from '../common/get-current-user.js';
|
||||
|
||||
const verifyCredentials = async ($) => {
|
||||
if ($.auth.data.originalState !== $.auth.data.state) {
|
||||
throw new Error("The 'state' parameter does not match.");
|
||||
}
|
||||
if ($.auth.data.originalCodeChallenge !== $.auth.data.code_challenge) {
|
||||
throw new Error("The 'code challenge' parameter does not match.");
|
||||
}
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value;
|
||||
const basicAuthToken = Buffer.from(
|
||||
$.auth.data.clientId + ':' + $.auth.data.clientSecret
|
||||
).toString('base64');
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'https://airtable.com/oauth2/v1/token',
|
||||
{
|
||||
code: $.auth.data.code,
|
||||
client_id: $.auth.data.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
code_verifier: $.auth.data.codeVerifier,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
},
|
||||
additionalProperties: {
|
||||
skipAddingAuthHeader: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
|
||||
const currentUser = await getCurrentUser($);
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
clientSecret: $.auth.data.clientSecret,
|
||||
scope: $.auth.data.scope,
|
||||
expiresIn: data.expires_in,
|
||||
refreshExpiresIn: data.refresh_expires_in,
|
||||
refreshToken: data.refresh_token,
|
||||
screenName: currentUser.email,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
12
packages/backend/src/apps/airtable/common/add-auth-header.js
Normal file
12
packages/backend/src/apps/airtable/common/add-auth-header.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const addAuthHeader = ($, requestConfig) => {
|
||||
if (
|
||||
!requestConfig.additionalProperties?.skipAddingAuthHeader &&
|
||||
$.auth.data?.accessToken
|
||||
) {
|
||||
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
12
packages/backend/src/apps/airtable/common/auth-scope.js
Normal file
12
packages/backend/src/apps/airtable/common/auth-scope.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const authScope = [
|
||||
'data.records:read',
|
||||
'data.records:write',
|
||||
'data.recordComments:read',
|
||||
'data.recordComments:write',
|
||||
'schema.bases:read',
|
||||
'schema.bases:write',
|
||||
'user.email:read',
|
||||
'webhook:manage',
|
||||
];
|
||||
|
||||
export default authScope;
|
@@ -0,0 +1,6 @@
|
||||
const getCurrentUser = async ($) => {
|
||||
const { data: currentUser } = await $.http.get('/v0/meta/whoami');
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
6
packages/backend/src/apps/airtable/dynamic-data/index.js
Normal file
6
packages/backend/src/apps/airtable/dynamic-data/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import listBases from './list-bases/index.js';
|
||||
import listTableFields from './list-table-fields/index.js';
|
||||
import listTableViews from './list-table-views/index.js';
|
||||
import listTables from './list-tables/index.js';
|
||||
|
||||
export default [listBases, listTableFields, listTableViews, listTables];
|
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
name: 'List bases',
|
||||
key: 'listBases',
|
||||
|
||||
async run($) {
|
||||
const bases = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = {};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get('/v0/meta/bases', { params });
|
||||
params.offset = data.offset;
|
||||
|
||||
if (data?.bases) {
|
||||
for (const base of data.bases) {
|
||||
bases.data.push({
|
||||
value: base.id,
|
||||
name: base.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.offset);
|
||||
|
||||
return bases;
|
||||
},
|
||||
};
|
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
name: 'List table fields',
|
||||
key: 'listTableFields',
|
||||
|
||||
async run($) {
|
||||
const tableFields = {
|
||||
data: [],
|
||||
};
|
||||
const { baseId, tableId } = $.step.parameters;
|
||||
|
||||
if (!baseId) {
|
||||
return tableFields;
|
||||
}
|
||||
|
||||
const params = {};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||
params,
|
||||
});
|
||||
params.offset = data.offset;
|
||||
|
||||
if (data?.tables) {
|
||||
for (const table of data.tables) {
|
||||
if (table.id === tableId) {
|
||||
table.fields.forEach((field) => {
|
||||
tableFields.data.push({
|
||||
value: field.name,
|
||||
name: field.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (params.offset);
|
||||
|
||||
return tableFields;
|
||||
},
|
||||
};
|
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
name: 'List table views',
|
||||
key: 'listTableViews',
|
||||
|
||||
async run($) {
|
||||
const tableViews = {
|
||||
data: [],
|
||||
};
|
||||
const { baseId, tableId } = $.step.parameters;
|
||||
|
||||
if (!baseId) {
|
||||
return tableViews;
|
||||
}
|
||||
|
||||
const params = {};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||
params,
|
||||
});
|
||||
params.offset = data.offset;
|
||||
|
||||
if (data?.tables) {
|
||||
for (const table of data.tables) {
|
||||
if (table.id === tableId) {
|
||||
table.views.forEach((view) => {
|
||||
tableViews.data.push({
|
||||
value: view.id,
|
||||
name: view.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (params.offset);
|
||||
|
||||
return tableViews;
|
||||
},
|
||||
};
|
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
name: 'List tables',
|
||||
key: 'listTables',
|
||||
|
||||
async run($) {
|
||||
const tables = {
|
||||
data: [],
|
||||
};
|
||||
const baseId = $.step.parameters.baseId;
|
||||
|
||||
if (!baseId) {
|
||||
return tables;
|
||||
}
|
||||
|
||||
const params = {};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
|
||||
params,
|
||||
});
|
||||
params.offset = data.offset;
|
||||
|
||||
if (data?.tables) {
|
||||
for (const table of data.tables) {
|
||||
tables.data.push({
|
||||
value: table.id,
|
||||
name: table.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.offset);
|
||||
|
||||
return tables;
|
||||
},
|
||||
};
|
@@ -0,0 +1,3 @@
|
||||
import listFields from './list-fields/index.js';
|
||||
|
||||
export default [listFields];
|
@@ -0,0 +1,86 @@
|
||||
const hasValue = (value) => value !== null && value !== undefined;
|
||||
|
||||
export default {
|
||||
name: 'List fields',
|
||||
key: 'listFields',
|
||||
|
||||
async run($) {
|
||||
const options = [];
|
||||
const { baseId, tableId } = $.step.parameters;
|
||||
|
||||
if (!hasValue(baseId) || !hasValue(tableId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`);
|
||||
|
||||
const selectedTable = data.tables.find((table) => table.id === tableId);
|
||||
|
||||
if (!selectedTable) return;
|
||||
|
||||
selectedTable.fields.forEach((field) => {
|
||||
if (field.type === 'singleSelect') {
|
||||
options.push({
|
||||
label: field.name,
|
||||
key: field.name,
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
variables: true,
|
||||
options: field.options.choices.map((choice) => ({
|
||||
label: choice.name,
|
||||
value: choice.id,
|
||||
})),
|
||||
});
|
||||
} else if (field.type === 'multipleSelects') {
|
||||
options.push({
|
||||
label: field.name,
|
||||
key: field.name,
|
||||
type: 'dynamic',
|
||||
required: false,
|
||||
variables: true,
|
||||
fields: [
|
||||
{
|
||||
label: 'Value',
|
||||
key: 'value',
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
variables: true,
|
||||
options: field.options.choices.map((choice) => ({
|
||||
label: choice.name,
|
||||
value: choice.id,
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
} else if (field.type === 'checkbox') {
|
||||
options.push({
|
||||
label: field.name,
|
||||
key: field.name,
|
||||
type: 'dropdown',
|
||||
required: false,
|
||||
variables: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: 'true',
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
value: 'false',
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
label: field.name,
|
||||
key: field.name,
|
||||
type: 'string',
|
||||
required: false,
|
||||
variables: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
},
|
||||
};
|
22
packages/backend/src/apps/airtable/index.js
Normal file
22
packages/backend/src/apps/airtable/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import defineApp from '../../helpers/define-app.js';
|
||||
import addAuthHeader from './common/add-auth-header.js';
|
||||
import auth from './auth/index.js';
|
||||
import actions from './actions/index.js';
|
||||
import dynamicData from './dynamic-data/index.js';
|
||||
import dynamicFields from './dynamic-fields/index.js';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Airtable',
|
||||
key: 'airtable',
|
||||
baseUrl: 'https://airtable.com',
|
||||
apiBaseUrl: 'https://api.airtable.com',
|
||||
iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg',
|
||||
authDocUrl: '{DOCS_URL}/apps/airtable/connection',
|
||||
primaryColor: 'FFBF00',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
dynamicData,
|
||||
dynamicFields,
|
||||
});
|
1
packages/backend/src/apps/appwrite/assets/favicon.svg
Normal file
1
packages/backend/src/apps/appwrite/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="132" height="24" fill="none" viewBox="0 0 132 24"><path fill="#19191C" d="M38.557 19.495c2.16 0 3.25-1.113 3.725-1.87h.214c.094.805.664 1.562 1.779 1.562h2.111V16.82h-.545c-.38 0-.57-.213-.57-.545V6.776h-2.8v1.516h-.213c-.545-.758-1.684-1.824-3.772-1.824-3.321 0-5.789 2.748-5.789 6.514s2.515 6.513 5.86 6.513m.498-2.7c-1.969 0-3.51-1.445-3.51-3.79 0-2.297 1.494-3.86 3.487-3.86 1.898 0 3.487 1.397 3.487 3.86 0 2.108-1.352 3.79-3.463 3.79M48.04 24h2.799v-6.376h.213c.522.758 1.637 1.871 3.844 1.871 3.321 0 5.741-2.795 5.741-6.513 0-3.743-2.586-6.514-5.931-6.514-2.135 0-3.18 1.16-3.678 1.8h-.213V6.776h-2.776V24m6.263-7.134c-1.922 0-3.512-1.42-3.512-3.884 0-2.108 1.353-3.885 3.464-3.885 1.97 0 3.511 1.54 3.511 3.885 0 2.297-1.494 3.884-3.463 3.884M62.082 24h2.8v-6.376h.213c.522.758 1.637 1.871 3.843 1.871 3.321 0 5.51-2.795 5.51-6.513 0-3.743-2.355-6.514-5.7-6.514-2.135 0-3.179 1.16-3.677 1.8h-.214V6.776h-2.775zm6.263-7.134c-1.922 0-3.511-1.42-3.511-3.884 0-2.108 1.352-3.885 3.463-3.885 1.97 0 3.512 1.54 3.512 3.885 0 2.297-1.495 3.884-3.464 3.884m9.805 2.61h3.961l2.254-9.735h.143l2.253 9.735H90.7l3.153-12.412h-2.821l-2.254 9.759h-.214l-2.253-9.759h-3.725l-2.278 9.759h-.213l-2.23-9.759h-2.99l3.274 12.412m17.123 0h2.8V13.34c0-2.345 1.09-3.79 3.131-3.79h1.233V6.756h-.925c-1.59 0-2.8 1.09-3.274 2.132h-.19V7.064h-2.775zm21.057 0h2.183v-2.487h-2.159c-.854 0-1.21-.38-1.21-1.256V9.528h3.511V7.064h-3.511V3.582h-2.657v3.482h-2.325v2.464h2.159v6.229c0 2.63 1.589 3.719 4.009 3.719m9.693.019c2.586 0 4.864-1.279 5.67-3.86l-2.562-.616c-.451 1.373-1.755 2.084-3.131 2.084-2.041 0-3.393-1.326-3.417-3.41h9.419v-.782c0-3.695-2.301-6.443-6.097-6.443-3.346 0-6.216 2.63-6.216 6.537 0 3.79 2.538 6.49 6.334 6.49m-3.416-7.84c.166-1.492 1.518-2.747 3.298-2.747 1.708 0 3.108 1.066 3.25 2.747h-6.548"/><path fill="#19191C" fill-rule="evenodd" d="M108.916 19.476h-2.8V9.528h-2.182V7.064h4.982z" clip-rule="evenodd"/><path fill="#19191C" d="M107.309 5.342c1.02 0 1.779-.758 1.779-1.753 0-.971-.759-1.73-1.779-1.73-1.021 0-1.78.759-1.78 1.73 0 .995.759 1.753 1.78 1.753"/><path fill="#FD366E" d="M24.443 16.432v5.478H10.752c-3.989 0-7.472-2.203-9.335-5.478A11.041 11.041 0 0 1 0 11.695v-1.48a10.97 10.97 0 0 1 .381-2.247C1.661 3.368 5.82 0 10.751 0c4.934 0 9.092 3.37 10.371 7.967h-5.854c-.96-1.499-2.624-2.49-4.516-2.49s-3.555.991-4.516 2.49a5.47 5.47 0 0 0-.67 1.494 5.562 5.562 0 0 0-.202 1.494 5.5 5.5 0 0 0 1.69 3.983 5.32 5.32 0 0 0 3.698 1.494h13.69"/><path fill="#FD366E" d="M24.443 9.46v5.478h-9.994a5.5 5.5 0 0 0 1.691-3.983 5.56 5.56 0 0 0-.203-1.494h8.506"/></svg>
|
After Width: | Height: | Size: 2.6 KiB |
65
packages/backend/src/apps/appwrite/auth/index.js
Normal file
65
packages/backend/src/apps/appwrite/auth/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
import isStillVerified from './is-still-verified.js';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'screenName',
|
||||
label: 'Screen Name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Screen name of your connection to be used on Automatisch UI.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'projectId',
|
||||
label: 'Project ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Project ID of your Appwrite project.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'API key of your Appwrite project.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'instanceUrl',
|
||||
label: 'Appwrite instance URL',
|
||||
type: 'string',
|
||||
required: false,
|
||||
readOnly: false,
|
||||
placeholder: '',
|
||||
description: '',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'host',
|
||||
label: 'Host Name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Host name of your Appwrite project.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,5 @@
|
||||
const verifyCredentials = async ($) => {
|
||||
await $.http.get('/v1/users');
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
16
packages/backend/src/apps/appwrite/common/add-auth-header.js
Normal file
16
packages/backend/src/apps/appwrite/common/add-auth-header.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const addAuthHeader = ($, requestConfig) => {
|
||||
requestConfig.headers['Content-Type'] = 'application/json';
|
||||
|
||||
if ($.auth.data?.apiKey && $.auth.data?.projectId) {
|
||||
requestConfig.headers['X-Appwrite-Project'] = $.auth.data.projectId;
|
||||
requestConfig.headers['X-Appwrite-Key'] = $.auth.data.apiKey;
|
||||
}
|
||||
|
||||
if ($.auth.data?.host) {
|
||||
requestConfig.headers['Host'] = $.auth.data.host;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
13
packages/backend/src/apps/appwrite/common/set-base-url.js
Normal file
13
packages/backend/src/apps/appwrite/common/set-base-url.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const setBaseUrl = ($, requestConfig) => {
|
||||
const instanceUrl = $.auth.data.instanceUrl;
|
||||
|
||||
if (instanceUrl) {
|
||||
requestConfig.baseURL = instanceUrl;
|
||||
} else if ($.app.apiBaseUrl) {
|
||||
requestConfig.baseURL = $.app.apiBaseUrl;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default setBaseUrl;
|
4
packages/backend/src/apps/appwrite/dynamic-data/index.js
Normal file
4
packages/backend/src/apps/appwrite/dynamic-data/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import listCollections from './list-collections/index.js';
|
||||
import listDatabases from './list-databases/index.js';
|
||||
|
||||
export default [listCollections, listDatabases];
|
@@ -0,0 +1,44 @@
|
||||
export default {
|
||||
name: 'List collections',
|
||||
key: 'listCollections',
|
||||
|
||||
async run($) {
|
||||
const collections = {
|
||||
data: [],
|
||||
};
|
||||
const databaseId = $.step.parameters.databaseId;
|
||||
|
||||
if (!databaseId) {
|
||||
return collections;
|
||||
}
|
||||
|
||||
const params = {
|
||||
queries: [
|
||||
JSON.stringify({
|
||||
method: 'orderAsc',
|
||||
attribute: 'name',
|
||||
}),
|
||||
JSON.stringify({
|
||||
method: 'limit',
|
||||
values: [100],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get(
|
||||
`/v1/databases/${databaseId}/collections`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
if (data?.collections) {
|
||||
for (const collection of data.collections) {
|
||||
collections.data.push({
|
||||
value: collection.$id,
|
||||
name: collection.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return collections;
|
||||
},
|
||||
};
|
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
name: 'List databases',
|
||||
key: 'listDatabases',
|
||||
|
||||
async run($) {
|
||||
const databases = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = {
|
||||
queries: [
|
||||
JSON.stringify({
|
||||
method: 'orderAsc',
|
||||
attribute: 'name',
|
||||
}),
|
||||
JSON.stringify({
|
||||
method: 'limit',
|
||||
values: [100],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get('/v1/databases', { params });
|
||||
|
||||
if (data?.databases) {
|
||||
for (const database of data.databases) {
|
||||
databases.data.push({
|
||||
value: database.$id,
|
||||
name: database.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return databases;
|
||||
},
|
||||
};
|
21
packages/backend/src/apps/appwrite/index.js
Normal file
21
packages/backend/src/apps/appwrite/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import defineApp from '../../helpers/define-app.js';
|
||||
import addAuthHeader from './common/add-auth-header.js';
|
||||
import setBaseUrl from './common/set-base-url.js';
|
||||
import auth from './auth/index.js';
|
||||
import triggers from './triggers/index.js';
|
||||
import dynamicData from './dynamic-data/index.js';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Appwrite',
|
||||
key: 'appwrite',
|
||||
baseUrl: 'https://appwrite.io',
|
||||
apiBaseUrl: 'https://cloud.appwrite.io',
|
||||
iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg',
|
||||
authDocUrl: '{DOCS_URL}/apps/appwrite/connection',
|
||||
primaryColor: 'FD366E',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [setBaseUrl, addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
3
packages/backend/src/apps/appwrite/triggers/index.js
Normal file
3
packages/backend/src/apps/appwrite/triggers/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import newDocuments from './new-documents/index.js';
|
||||
|
||||
export default [newDocuments];
|
@@ -0,0 +1,104 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger.js';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New documents',
|
||||
key: 'newDocuments',
|
||||
pollInterval: 15,
|
||||
description: 'Triggers when a new document is created.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Database',
|
||||
key: 'databaseId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDatabases',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Collection',
|
||||
key: 'collectionId',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
dependsOn: ['parameters.databaseId'],
|
||||
description: '',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listCollections',
|
||||
},
|
||||
{
|
||||
name: 'parameters.databaseId',
|
||||
value: '{parameters.databaseId}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const { databaseId, collectionId } = $.step.parameters;
|
||||
|
||||
const limit = 1;
|
||||
let lastDocumentId = undefined;
|
||||
let offset = 0;
|
||||
let documentCount = 0;
|
||||
|
||||
do {
|
||||
const params = {
|
||||
queries: [
|
||||
JSON.stringify({
|
||||
method: 'orderDesc',
|
||||
attribute: '$createdAt',
|
||||
}),
|
||||
JSON.stringify({
|
||||
method: 'limit',
|
||||
values: [limit],
|
||||
}),
|
||||
// An invalid cursor shouldn't be sent.
|
||||
lastDocumentId &&
|
||||
JSON.stringify({
|
||||
method: 'cursorAfter',
|
||||
values: [lastDocumentId],
|
||||
}),
|
||||
].filter(Boolean),
|
||||
};
|
||||
|
||||
const { data } = await $.http.get(
|
||||
`/v1/databases/${databaseId}/collections/${collectionId}/documents`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
const documents = data?.documents;
|
||||
documentCount = documents?.length;
|
||||
offset = offset + limit;
|
||||
lastDocumentId = documents[documentCount - 1]?.$id;
|
||||
|
||||
if (!documentCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const document of documents) {
|
||||
$.pushTriggerItem({
|
||||
raw: document,
|
||||
meta: {
|
||||
internalId: document.$id,
|
||||
},
|
||||
});
|
||||
}
|
||||
} while (documentCount === limit);
|
||||
},
|
||||
});
|
@@ -0,0 +1,41 @@
|
||||
import { BskyAgent, RichText } from '@atproto/api';
|
||||
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create post',
|
||||
key: 'createPost',
|
||||
description: 'Creates a new post.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Text',
|
||||
key: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
variables: true,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const text = $.step.parameters.text;
|
||||
const agent = new BskyAgent({ service: 'https://bsky.social/xrpc' });
|
||||
const richText = new RichText({ text });
|
||||
await richText.detectFacets(agent);
|
||||
|
||||
const body = {
|
||||
repo: $.auth.data.did,
|
||||
collection: 'app.bsky.feed.post',
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: richText.text,
|
||||
facets: richText.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
const { data } = await $.http.post('/com.atproto.repo.createRecord', body);
|
||||
|
||||
$.setActionItem({ raw: data });
|
||||
},
|
||||
});
|
5
packages/backend/src/apps/bluesky/actions/index.js
Normal file
5
packages/backend/src/apps/bluesky/actions/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import createPost from './create-post/index.js';
|
||||
import replyToPost from './reply-to-post/index.js';
|
||||
import searchPostByUrl from './search-post-by-url/index.js';
|
||||
|
||||
export default [createPost, replyToPost, searchPostByUrl];
|
@@ -0,0 +1,55 @@
|
||||
import { BskyAgent, RichText } from '@atproto/api';
|
||||
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
import getReplyRefs from '../../common/get-reply-refs.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Reply to post',
|
||||
key: 'replyToPost',
|
||||
description: 'Replies to a post.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Post Url',
|
||||
key: 'postUrl',
|
||||
type: 'string',
|
||||
required: true,
|
||||
variables: true,
|
||||
description: 'Enter whole post url you want to reply here.',
|
||||
},
|
||||
{
|
||||
label: 'Text',
|
||||
key: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
variables: true,
|
||||
description: 'Your post.',
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const text = $.step.parameters.text;
|
||||
const postUrl = $.step.parameters.postUrl;
|
||||
|
||||
const replyRefs = await getReplyRefs($, postUrl);
|
||||
|
||||
const agent = new BskyAgent({ service: 'https://bsky.social/xrpc' });
|
||||
const richText = new RichText({ text });
|
||||
await richText.detectFacets(agent);
|
||||
|
||||
const body = {
|
||||
repo: $.auth.data.did,
|
||||
collection: 'app.bsky.feed.post',
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: richText.text,
|
||||
facets: richText.facets,
|
||||
createdAt: new Date().toISOString(),
|
||||
reply: replyRefs,
|
||||
},
|
||||
};
|
||||
|
||||
const { data } = await $.http.post('/com.atproto.repo.createRecord', body);
|
||||
|
||||
$.setActionItem({ raw: data });
|
||||
},
|
||||
});
|
@@ -0,0 +1,35 @@
|
||||
import defineAction from '../../../../helpers/define-action.js';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Search post by url',
|
||||
key: 'searchPostByUrl',
|
||||
description: 'Searches a post in a thread by url.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Post Url',
|
||||
key: 'postUrl',
|
||||
type: 'string',
|
||||
required: true,
|
||||
variables: true,
|
||||
description: 'Enter whole post url here.',
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const postUrl = $.step.parameters.postUrl;
|
||||
const urlParts = postUrl.split('/');
|
||||
const handle = urlParts[urlParts.length - 3];
|
||||
const postId = urlParts[urlParts.length - 1];
|
||||
const uri = `at://${handle}/app.bsky.feed.post/${postId}`;
|
||||
|
||||
const params = {
|
||||
uri,
|
||||
};
|
||||
|
||||
const { data } = await $.http.get('/app.bsky.feed.getPostThread', {
|
||||
params,
|
||||
});
|
||||
|
||||
$.setActionItem({ raw: data });
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/bluesky/assets/favicon.svg
Normal file
4
packages/backend/src/apps/bluesky/assets/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
|
||||
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 956 B |
34
packages/backend/src/apps/bluesky/auth/index.js
Normal file
34
packages/backend/src/apps/bluesky/auth/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import verifyCredentials from './verify-credentials.js';
|
||||
import isStillVerified from './is-still-verified.js';
|
||||
import refreshToken from './refresh-token.js';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'handle',
|
||||
label: 'Your Bluesky Handle',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: '',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Your Bluesky Password',
|
||||
type: 'string',
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: '',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,8 @@
|
||||
import getCurrentUser from '../common/get-current-user.js';
|
||||
|
||||
const isStillVerified = async ($) => {
|
||||
const currentUser = await getCurrentUser($);
|
||||
return !!currentUser.did;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
24
packages/backend/src/apps/bluesky/auth/refresh-token.js
Normal file
24
packages/backend/src/apps/bluesky/auth/refresh-token.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const refreshToken = async ($) => {
|
||||
const { refreshJwt } = $.auth.data;
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'/com.atproto.server.refreshSession',
|
||||
null,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${refreshJwt}`,
|
||||
},
|
||||
additionalProperties: {
|
||||
skipAddingAuthHeader: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessJwt: data.accessJwt,
|
||||
refreshJwt: data.refreshJwt,
|
||||
did: data.did,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
20
packages/backend/src/apps/bluesky/auth/verify-credentials.js
Normal file
20
packages/backend/src/apps/bluesky/auth/verify-credentials.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const verifyCredentials = async ($) => {
|
||||
const handle = $.auth.data.handle;
|
||||
const password = $.auth.data.password;
|
||||
|
||||
const body = {
|
||||
identifier: handle,
|
||||
password,
|
||||
};
|
||||
|
||||
const { data } = await $.http.post('/com.atproto.server.createSession', body);
|
||||
|
||||
await $.auth.set({
|
||||
accessJwt: data.accessJwt,
|
||||
refreshJwt: data.refreshJwt,
|
||||
did: data.did,
|
||||
screenName: data.handle,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
12
packages/backend/src/apps/bluesky/common/add-auth-header.js
Normal file
12
packages/backend/src/apps/bluesky/common/add-auth-header.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const addAuthHeader = ($, requestConfig) => {
|
||||
if (requestConfig.additionalProperties?.skipAddingAuthHeader)
|
||||
return requestConfig;
|
||||
|
||||
if ($.auth.data?.accessJwt) {
|
||||
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessJwt}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
15
packages/backend/src/apps/bluesky/common/get-current-user.js
Normal file
15
packages/backend/src/apps/bluesky/common/get-current-user.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const getCurrentUser = async ($) => {
|
||||
const handle = $.auth.data.handle;
|
||||
|
||||
const params = {
|
||||
actor: handle,
|
||||
};
|
||||
|
||||
const { data: currentUser } = await $.http.get('/app.bsky.actor.getProfile', {
|
||||
params,
|
||||
});
|
||||
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
58
packages/backend/src/apps/bluesky/common/get-reply-refs.js
Normal file
58
packages/backend/src/apps/bluesky/common/get-reply-refs.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const parseUri = (uri) => {
|
||||
const parts = uri.split('/');
|
||||
|
||||
return {
|
||||
repo: parts[4],
|
||||
collection: 'app.bsky.feed.post',
|
||||
rkey: parts[6],
|
||||
};
|
||||
};
|
||||
|
||||
const getReplyRefs = async ($, parentUri) => {
|
||||
const uriParts = parseUri(parentUri);
|
||||
|
||||
try {
|
||||
const { data: parent } = await $.http.get('/com.atproto.repo.getRecord', {
|
||||
params: uriParts,
|
||||
});
|
||||
|
||||
const parentReply = parent.value?.reply;
|
||||
let root;
|
||||
|
||||
if (parentReply) {
|
||||
const rootUri = parentReply.root.uri;
|
||||
const [rootRepo, rootCollection, rootRkey] = rootUri
|
||||
.split('/')
|
||||
.slice(2, 5);
|
||||
|
||||
const params = {
|
||||
repo: rootRepo,
|
||||
collection: rootCollection,
|
||||
rkey: rootRkey,
|
||||
};
|
||||
|
||||
const rootResp = await $.http.get('/com.atproto.repo.getRecord', {
|
||||
params,
|
||||
});
|
||||
|
||||
root = rootResp.data;
|
||||
} else {
|
||||
root = parent;
|
||||
}
|
||||
|
||||
return {
|
||||
root: {
|
||||
uri: root.uri,
|
||||
cid: root.cid,
|
||||
},
|
||||
parent: {
|
||||
uri: parent.uri,
|
||||
cid: parent.cid,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error('Error while fetching records');
|
||||
}
|
||||
};
|
||||
|
||||
export default getReplyRefs;
|
18
packages/backend/src/apps/bluesky/index.js
Normal file
18
packages/backend/src/apps/bluesky/index.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import defineApp from '../../helpers/define-app.js';
|
||||
import addAuthHeader from './common/add-auth-header.js';
|
||||
import auth from './auth/index.js';
|
||||
import actions from './actions/index.js';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Bluesky',
|
||||
key: 'bluesky',
|
||||
iconUrl: '{BASE_URL}/apps/bluesky/assets/favicon.svg',
|
||||
authDocUrl: '{DOCS_URL}/apps/bluesky/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://bluesky.app',
|
||||
apiBaseUrl: 'https://bsky.social/xrpc',
|
||||
primaryColor: '1185fd',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
});
|
@@ -5,11 +5,24 @@ const formatDateTime = ($) => {
|
||||
|
||||
const fromFormat = $.step.parameters.fromFormat;
|
||||
const fromTimezone = $.step.parameters.fromTimezone;
|
||||
let inputDateTime;
|
||||
|
||||
const inputDateTime = DateTime.fromFormat(input, fromFormat, {
|
||||
zone: fromTimezone,
|
||||
setZone: true,
|
||||
});
|
||||
if (fromFormat === 'X') {
|
||||
inputDateTime = DateTime.fromSeconds(Number(input), fromFormat, {
|
||||
zone: fromTimezone,
|
||||
setZone: true,
|
||||
});
|
||||
} else if (fromFormat === 'x') {
|
||||
inputDateTime = DateTime.fromMillis(Number(input), fromFormat, {
|
||||
zone: fromTimezone,
|
||||
setZone: true,
|
||||
});
|
||||
} else {
|
||||
inputDateTime = DateTime.fromFormat(input, fromFormat, {
|
||||
zone: fromTimezone,
|
||||
setZone: true,
|
||||
});
|
||||
}
|
||||
|
||||
const toFormat = $.step.parameters.toFormat;
|
||||
const toTimezone = $.step.parameters.toTimezone;
|
||||
|
@@ -26,12 +26,39 @@ export default defineConfig({
|
||||
},
|
||||
{
|
||||
text: 'Apps',
|
||||
link: '/apps/carbone/connection',
|
||||
link: '/apps/airtable/connection',
|
||||
activeMatch: '/apps/',
|
||||
},
|
||||
],
|
||||
sidebar: {
|
||||
'/apps/': [
|
||||
{
|
||||
text: 'Airtable',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Actions', link: '/apps/airtable/actions' },
|
||||
{ text: 'Connection', link: '/apps/airtable/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Appwrite',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/appwrite/triggers' },
|
||||
{ text: 'Connection', link: '/apps/appwrite/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Bluesky',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Actions', link: '/apps/bluesky/actions' },
|
||||
{ text: 'Connection', link: '/apps/bluesky/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Carbone',
|
||||
collapsible: true,
|
||||
|
14
packages/docs/pages/apps/airtable/actions.md
Normal file
14
packages/docs/pages/apps/airtable/actions.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
favicon: /favicons/airtable.svg
|
||||
items:
|
||||
- name: Create record
|
||||
desc: Creates a new record with fields that automatically populate.
|
||||
- name: Find record
|
||||
desc: Finds a record using simple field search or use Airtable's formula syntax to find a matching record.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
19
packages/docs/pages/apps/airtable/connection.md
Normal file
19
packages/docs/pages/apps/airtable/connection.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Airtable
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the Airtable
|
||||
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
1. Login to your [Airtable account](https://www.airtable.com/).
|
||||
2. Go to this [link](https://airtable.com/create/oauth) and click on the **Register new OAuth integration**.
|
||||
3. Fill the name field.
|
||||
4. Copy **OAuth Redirect URL** from Automatisch to **OAuth redirect URL** field.
|
||||
5. Click on the **Register integration** button.
|
||||
6. In **Developer Details** section, click on the **Generate client secret**.
|
||||
7. Check the checkboxes of **Scopes** section.
|
||||
8. Click on the **Save changes** button.
|
||||
9. Copy **Client ID** to **Client ID** field on Automatisch.
|
||||
10. Copy **Client secret** to **Client secret** field on Automatisch.
|
||||
11. Click **Submit** button on Automatisch.
|
||||
12. Congrats! Start using your new Airtable connection within the flows.
|
20
packages/docs/pages/apps/appwrite/connection.md
Normal file
20
packages/docs/pages/apps/appwrite/connection.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Appwrite
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the Appwrite
|
||||
connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
1. Login to your Appwrite account: [https://appwrite.io/](https://appwrite.io/).
|
||||
2. Go to your project's **Settings**.
|
||||
3. In the Settings, click on the **View API Keys** button in **API credentials** section.
|
||||
4. Click on the **Create API Key** button.
|
||||
5. Fill the name field and select **Never** for the expiration date.
|
||||
6. Click on the **Next** button.
|
||||
7. Click on the **Select all** and then click on the **Create** button.
|
||||
8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch.
|
||||
9. Write any screen name to be displayed in Automatisch.
|
||||
10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich.
|
||||
11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch.
|
||||
12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL.
|
||||
13. Start using Appwrite integration with Automatisch!
|
12
packages/docs/pages/apps/appwrite/triggers.md
Normal file
12
packages/docs/pages/apps/appwrite/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
favicon: /favicons/appwrite.svg
|
||||
items:
|
||||
- name: New documets
|
||||
desc: Triggers when a new document is created.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
16
packages/docs/pages/apps/bluesky/actions.md
Normal file
16
packages/docs/pages/apps/bluesky/actions.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
favicon: /favicons/bluesky.svg
|
||||
items:
|
||||
- name: Create post
|
||||
desc: Creates a new post.
|
||||
- name: Reply to post
|
||||
desc: Replies to a post.
|
||||
- name: Search post by url
|
||||
desc: Searches a post in a thread by url.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
10
packages/docs/pages/apps/bluesky/connection.md
Normal file
10
packages/docs/pages/apps/bluesky/connection.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Bluesky
|
||||
|
||||
:::info
|
||||
This page explains the steps you need to follow to set up the Bluesky connection in Automatisch. If any of the steps are outdated, please let us know!
|
||||
:::
|
||||
|
||||
1. Enter your `Bluesky Handle` from the page to the `Your Bluesky Handle` field on Automatisch.
|
||||
1. Enter your `Bluesky Password` from the page to the `Your Bluesky Password` field on Automatisch.
|
||||
1. Click **Submit** button on Automatisch.
|
||||
1. Congrats! Start using your new Bluesky connection within the flows.
|
@@ -2,6 +2,9 @@
|
||||
|
||||
The following integrations are currently supported by Automatisch.
|
||||
|
||||
- [Airtable](/apps/airtable/actions)
|
||||
- [Appwrite](/apps/appwrite/triggers)
|
||||
- [Bluesky](/apps/bluesky/actions)
|
||||
- [Carbone](/apps/carbone/actions)
|
||||
- [Datastore](/apps/datastore/actions)
|
||||
- [DeepL](/apps/deepl/actions)
|
||||
|
9
packages/docs/pages/public/favicons/airtable.svg
Normal file
9
packages/docs/pages/public/favicons/airtable.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="256px" height="215px" viewBox="0 0 256 215" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M114.25873,2.70101695 L18.8604023,42.1756384 C13.5552723,44.3711638 13.6102328,51.9065311 18.9486282,54.0225085 L114.746142,92.0117514 C123.163769,95.3498757 132.537419,95.3498757 140.9536,92.0117514 L236.75256,54.0225085 C242.08951,51.9065311 242.145916,44.3711638 236.83934,42.1756384 L141.442459,2.70101695 C132.738459,-0.900338983 122.961284,-0.900338983 114.25873,2.70101695" fill="#FFBF00"></path>
|
||||
<path d="M136.349071,112.756863 L136.349071,207.659101 C136.349071,212.173089 140.900664,215.263892 145.096461,213.600615 L251.844122,172.166219 C254.281184,171.200072 255.879376,168.845451 255.879376,166.224705 L255.879376,71.3224678 C255.879376,66.8084791 251.327783,63.7176768 247.131986,65.3809537 L140.384325,106.815349 C137.94871,107.781496 136.349071,110.136118 136.349071,112.756863" fill="#26B5F8"></path>
|
||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L76.5257763,134.504714 L9.65047684,166.548104 C5.4112904,168.593211 0.000578531073,165.503855 0.000578531073,160.794612 L0.000578531073,71.7210757 C0.000578531073,70.0173017 0.874160452,68.5463864 2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill="#ED3049"></path>
|
||||
<path d="M111.422771,117.65355 L79.742409,132.949912 L2.04568588,67.4384994 C2.53454463,66.9481944 3.08848814,66.5446689 3.66412655,66.2250305 C5.26231864,65.2661153 7.54173107,65.0101153 9.47981017,65.7766689 L110.890522,105.957098 C116.045234,108.002206 116.450206,115.225166 111.422771,117.65355" fill-opacity="0.25" fill="#000000"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
packages/docs/pages/public/favicons/appwrite.svg
Normal file
1
packages/docs/pages/public/favicons/appwrite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="132" height="24" fill="none" viewBox="0 0 132 24"><path fill="#19191C" d="M38.557 19.495c2.16 0 3.25-1.113 3.725-1.87h.214c.094.805.664 1.562 1.779 1.562h2.111V16.82h-.545c-.38 0-.57-.213-.57-.545V6.776h-2.8v1.516h-.213c-.545-.758-1.684-1.824-3.772-1.824-3.321 0-5.789 2.748-5.789 6.514s2.515 6.513 5.86 6.513m.498-2.7c-1.969 0-3.51-1.445-3.51-3.79 0-2.297 1.494-3.86 3.487-3.86 1.898 0 3.487 1.397 3.487 3.86 0 2.108-1.352 3.79-3.463 3.79M48.04 24h2.799v-6.376h.213c.522.758 1.637 1.871 3.844 1.871 3.321 0 5.741-2.795 5.741-6.513 0-3.743-2.586-6.514-5.931-6.514-2.135 0-3.18 1.16-3.678 1.8h-.213V6.776h-2.776V24m6.263-7.134c-1.922 0-3.512-1.42-3.512-3.884 0-2.108 1.353-3.885 3.464-3.885 1.97 0 3.511 1.54 3.511 3.885 0 2.297-1.494 3.884-3.463 3.884M62.082 24h2.8v-6.376h.213c.522.758 1.637 1.871 3.843 1.871 3.321 0 5.51-2.795 5.51-6.513 0-3.743-2.355-6.514-5.7-6.514-2.135 0-3.179 1.16-3.677 1.8h-.214V6.776h-2.775zm6.263-7.134c-1.922 0-3.511-1.42-3.511-3.884 0-2.108 1.352-3.885 3.463-3.885 1.97 0 3.512 1.54 3.512 3.885 0 2.297-1.495 3.884-3.464 3.884m9.805 2.61h3.961l2.254-9.735h.143l2.253 9.735H90.7l3.153-12.412h-2.821l-2.254 9.759h-.214l-2.253-9.759h-3.725l-2.278 9.759h-.213l-2.23-9.759h-2.99l3.274 12.412m17.123 0h2.8V13.34c0-2.345 1.09-3.79 3.131-3.79h1.233V6.756h-.925c-1.59 0-2.8 1.09-3.274 2.132h-.19V7.064h-2.775zm21.057 0h2.183v-2.487h-2.159c-.854 0-1.21-.38-1.21-1.256V9.528h3.511V7.064h-3.511V3.582h-2.657v3.482h-2.325v2.464h2.159v6.229c0 2.63 1.589 3.719 4.009 3.719m9.693.019c2.586 0 4.864-1.279 5.67-3.86l-2.562-.616c-.451 1.373-1.755 2.084-3.131 2.084-2.041 0-3.393-1.326-3.417-3.41h9.419v-.782c0-3.695-2.301-6.443-6.097-6.443-3.346 0-6.216 2.63-6.216 6.537 0 3.79 2.538 6.49 6.334 6.49m-3.416-7.84c.166-1.492 1.518-2.747 3.298-2.747 1.708 0 3.108 1.066 3.25 2.747h-6.548"/><path fill="#19191C" fill-rule="evenodd" d="M108.916 19.476h-2.8V9.528h-2.182V7.064h4.982z" clip-rule="evenodd"/><path fill="#19191C" d="M107.309 5.342c1.02 0 1.779-.758 1.779-1.753 0-.971-.759-1.73-1.779-1.73-1.021 0-1.78.759-1.78 1.73 0 .995.759 1.753 1.78 1.753"/><path fill="#FD366E" d="M24.443 16.432v5.478H10.752c-3.989 0-7.472-2.203-9.335-5.478A11.041 11.041 0 0 1 0 11.695v-1.48a10.97 10.97 0 0 1 .381-2.247C1.661 3.368 5.82 0 10.751 0c4.934 0 9.092 3.37 10.371 7.967h-5.854c-.96-1.499-2.624-2.49-4.516-2.49s-3.555.991-4.516 2.49a5.47 5.47 0 0 0-.67 1.494 5.562 5.562 0 0 0-.202 1.494 5.5 5.5 0 0 0 1.69 3.983 5.32 5.32 0 0 0 3.698 1.494h13.69"/><path fill="#FD366E" d="M24.443 9.46v5.478h-9.994a5.5 5.5 0 0 0 1.691-3.983 5.56 5.56 0 0 0-.203-1.494h8.506"/></svg>
|
After Width: | Height: | Size: 2.6 KiB |
4
packages/docs/pages/public/favicons/bluesky.svg
Normal file
4
packages/docs/pages/public/favicons/bluesky.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
|
||||
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 956 B |
@@ -61,7 +61,7 @@ test.describe('Apps page', () => {
|
||||
|
||||
await applicationsPage.page.getByTestId('app-list-item').first().click();
|
||||
await expect(applicationsPage.page).toHaveURL(
|
||||
'/app/azure-openai/connections/add?shared=false'
|
||||
'/app/airtable/connections/add?shared=false'
|
||||
);
|
||||
await expect(
|
||||
applicationsPage.page.getByTestId('add-app-connection-dialog')
|
||||
@@ -75,14 +75,14 @@ test.describe('Apps page', () => {
|
||||
}) => {
|
||||
await applicationsPage.page.getByTestId('app-list-item').first().click();
|
||||
await expect(applicationsPage.page).toHaveURL(
|
||||
'/app/azure-openai/connections/add?shared=false'
|
||||
'/app/airtable/connections/add?shared=false'
|
||||
);
|
||||
await expect(
|
||||
applicationsPage.page.getByTestId('add-app-connection-dialog')
|
||||
).toBeVisible();
|
||||
await applicationsPage.clickAway();
|
||||
await expect(applicationsPage.page).toHaveURL(
|
||||
'/app/azure-openai/connections'
|
||||
'/app/airtable/connections'
|
||||
);
|
||||
await expect(
|
||||
applicationsPage.page.getByTestId('add-app-connection-dialog')
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
@@ -11,15 +12,18 @@ import AddIcon from '@mui/icons-material/Add';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
|
||||
const createGroupItem = () => ({
|
||||
key: '',
|
||||
operator: operators[0].value,
|
||||
value: '',
|
||||
id: uuidv4(),
|
||||
});
|
||||
|
||||
const createGroup = () => ({
|
||||
and: [createGroupItem()],
|
||||
});
|
||||
|
||||
const operators = [
|
||||
{
|
||||
label: 'Equal',
|
||||
@@ -54,6 +58,7 @@ const operators = [
|
||||
value: 'not_contains',
|
||||
},
|
||||
];
|
||||
|
||||
const createStringArgument = (argumentOptions) => {
|
||||
return {
|
||||
...argumentOptions,
|
||||
@@ -62,6 +67,7 @@ const createStringArgument = (argumentOptions) => {
|
||||
variables: true,
|
||||
};
|
||||
};
|
||||
|
||||
const createDropdownArgument = (argumentOptions) => {
|
||||
return {
|
||||
...argumentOptions,
|
||||
@@ -69,28 +75,35 @@ const createDropdownArgument = (argumentOptions) => {
|
||||
type: 'dropdown',
|
||||
};
|
||||
};
|
||||
|
||||
function FilterConditions(props) {
|
||||
const { stepId } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { control, setValue, getValues } = useFormContext();
|
||||
const groups = useWatch({ control, name: 'parameters.or' });
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
|
||||
React.useEffect(function addInitialGroupWhenEmpty() {
|
||||
const groups = getValues('parameters.or');
|
||||
|
||||
if (!groups) {
|
||||
setValue('parameters.or', [createGroup()]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const appendGroup = React.useCallback(() => {
|
||||
const values = getValues('parameters.or');
|
||||
setValue('parameters.or', values.concat(createGroup()));
|
||||
}, []);
|
||||
|
||||
const appendGroupItem = React.useCallback((index) => {
|
||||
const group = getValues(`parameters.or.${index}.and`);
|
||||
setValue(`parameters.or.${index}.and`, group.concat(createGroupItem()));
|
||||
}, []);
|
||||
|
||||
const removeGroupItem = React.useCallback((groupIndex, groupItemIndex) => {
|
||||
const group = getValues(`parameters.or.${groupIndex}.and`);
|
||||
|
||||
if (group.length === 1) {
|
||||
const groups = getValues('parameters.or');
|
||||
setValue(
|
||||
@@ -104,6 +117,7 @@ function FilterConditions(props) {
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Stack sx={{ width: '100%' }} direction="column" spacing={2} mt={2}>
|
||||
@@ -219,4 +233,9 @@ function FilterConditions(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
FilterConditions.propTypes = {
|
||||
stepId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FilterConditions;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
@@ -8,6 +9,8 @@ import { EditorContext } from 'contexts/Editor';
|
||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||
import InputCreator from 'components/InputCreator';
|
||||
import FilterConditions from './FilterConditions';
|
||||
import { StepPropType, SubstepPropType } from 'propTypes/propTypes';
|
||||
|
||||
function FlowSubstep(props) {
|
||||
const {
|
||||
substep,
|
||||
@@ -22,6 +25,7 @@ function FlowSubstep(props) {
|
||||
const formContext = useFormContext();
|
||||
const validationStatus = formContext.formState.isValid;
|
||||
const onToggle = expanded ? onCollapse : onExpand;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FlowSubstepTitle
|
||||
@@ -73,4 +77,14 @@ function FlowSubstep(props) {
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
FlowSubstep.propTypes = {
|
||||
substep: SubstepPropType.isRequired,
|
||||
expanded: PropTypes.bool,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onCollapse: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
step: StepPropType.isRequired,
|
||||
};
|
||||
|
||||
export default FlowSubstep;
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { ListItemButton, Typography } from './style';
|
||||
|
||||
const validIcon = <CheckCircleIcon color="success" />;
|
||||
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
function FlowSubstepTitle(props) {
|
||||
const { expanded = false, onClick = () => null, valid = null, title } = props;
|
||||
const hasValidation = valid !== null;
|
||||
const validationStatusIcon = valid ? validIcon : errorIcon;
|
||||
|
||||
return (
|
||||
<ListItemButton onClick={onClick} selected={expanded} divider>
|
||||
<Typography variant="body2">
|
||||
@@ -21,4 +26,12 @@ function FlowSubstepTitle(props) {
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
FlowSubstepTitle.propTypes = {
|
||||
expanded: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
valid: PropTypes.bool,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default FlowSubstepTitle;
|
||||
|
@@ -7,9 +7,11 @@ import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee';
|
||||
import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
|
||||
export default function ForgotPasswordForm() {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD);
|
||||
|
||||
const handleSubmit = async (values) => {
|
||||
await forgotPassword({
|
||||
variables: {
|
||||
@@ -17,6 +19,7 @@ export default function ForgotPasswordForm() {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ px: 2, py: 4 }}>
|
||||
<Typography
|
||||
|
78
yarn.lock
78
yarn.lock
@@ -170,6 +170,52 @@
|
||||
tslib "^2.3.0"
|
||||
zen-observable-ts "~1.1.0"
|
||||
|
||||
"@atproto/api@^0.12.13":
|
||||
version "0.12.13"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.13.tgz#269d6c57ea894e23f20b28bd3cbfed944bd28528"
|
||||
integrity sha512-pRSID6w8AUiZJoCxgctMPRTSGVFHq7wphAnxEbRLBP3OQ1g+BRZUcqFw+e+17Pd3wrc8VImjiD4HCWtCJvCx3w==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
"@atproto/xrpc" "^0.5.0"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
|
||||
"@atproto/common-web@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb"
|
||||
integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==
|
||||
dependencies:
|
||||
graphemer "^1.4.0"
|
||||
multiformats "^9.9.0"
|
||||
uint8arrays "3.0.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/lexicon@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
|
||||
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.3.0"
|
||||
"@atproto/syntax" "^0.3.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/syntax@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
|
||||
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
|
||||
|
||||
"@atproto/xrpc@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
|
||||
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "^0.4.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz"
|
||||
@@ -8939,6 +8985,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
||||
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz"
|
||||
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
|
||||
|
||||
graphemer@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
graphql-executor@0.0.18:
|
||||
version "0.0.18"
|
||||
resolved "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.18.tgz"
|
||||
@@ -9876,6 +9927,11 @@ isexe@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d"
|
||||
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
|
||||
|
||||
iso-datestring-validator@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895"
|
||||
integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
|
||||
|
||||
isobject@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
|
||||
@@ -11592,6 +11648,11 @@ multicast-dns@^6.0.1:
|
||||
dns-packet "^1.3.1"
|
||||
thunky "^1.0.2"
|
||||
|
||||
multiformats@^9.4.2, multiformats@^9.9.0:
|
||||
version "9.9.0"
|
||||
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
|
||||
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
|
||||
|
||||
multimatch@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz"
|
||||
@@ -15574,6 +15635,11 @@ tinyspy@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce"
|
||||
integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==
|
||||
|
||||
tlds@^1.234.0:
|
||||
version "1.252.0"
|
||||
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419"
|
||||
integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz"
|
||||
@@ -15823,6 +15889,13 @@ uid-number@0.0.6:
|
||||
resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz"
|
||||
integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
|
||||
|
||||
uint8arrays@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b"
|
||||
integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==
|
||||
dependencies:
|
||||
multiformats "^9.4.2"
|
||||
|
||||
umask@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz"
|
||||
@@ -16948,3 +17021,8 @@ zen-observable@0.8.15:
|
||||
version "0.8.15"
|
||||
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
|
||||
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
|
||||
|
||||
zod@^3.21.4:
|
||||
version "3.23.8"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||
|
Reference in New Issue
Block a user