├── .stylelintignore
├── storybook-vue
├── babel.config.js
├── .storybook
│ ├── preview-head.html
│ ├── webpack.config.js
│ ├── addons.js
│ └── config.js
├── package.json
└── stories
│ ├── index.js
│ └── helper.js
├── img
└── demo.gif
├── storybook-react
├── babel.config.js
├── .storybook
│ ├── preview-head.html
│ ├── webpack.config.js
│ ├── addons.js
│ └── config.js
├── package.json
└── stories
│ ├── helpers.jsx
│ └── index.jsx
├── vue.js
├── react.js
├── storybook-test
├── workflow.png
├── tests
│ ├── image
│ │ ├── __image_snapshots__
│ │ │ ├── desktop-test-js-desktop-image-tests-file-view-1-snap.png
│ │ │ └── mobile-test-js-mobile-image-tests-file-view-1-snap.png
│ │ ├── desktop.test.js
│ │ ├── mobile.test.js
│ │ └── core
│ │ │ ├── get_account_response.json
│ │ │ ├── folder_content_response.json
│ │ │ └── PuppeteerHelper.js
│ ├── setupTests.js
│ ├── globalTeardown.js
│ ├── integration
│ │ ├── core
│ │ │ └── ApiHelper.js
│ │ └── index.test.js
│ ├── testEnvironment.js
│ └── globalSetup.js
├── babel.config.js
├── .storybook
│ ├── preview-head.html
│ └── main.js
├── .eslintrc.js
├── stories
│ ├── basic.stories.js
│ └── core
│ │ └── components.js
├── static
│ └── style
│ │ └── icon.css
├── jest-puppeteer.config.js
├── package.json
├── jest.config.js
└── config.js
├── src
├── picker
│ ├── css
│ │ ├── constants.less
│ │ ├── dropdown.less
│ │ ├── files.less
│ │ ├── mkdir-form.less
│ │ ├── box.less
│ │ ├── list.less
│ │ ├── index.less
│ │ ├── selector.less
│ │ ├── accounts.less
│ │ ├── mixins.less
│ │ ├── breadcrumb.less
│ │ ├── material-icons-font.less
│ │ ├── buttons.less
│ │ ├── container.less
│ │ ├── global.less
│ │ ├── izitoast.less
│ │ ├── util.less
│ │ ├── filetable.less
│ │ └── icons.less
│ ├── templates
│ │ ├── index.pug
│ │ ├── dropzone.pug
│ │ ├── footer.pug
│ │ ├── picker.pug
│ │ ├── mkdir-form.pug
│ │ ├── computer.pug
│ │ ├── search.pug
│ │ ├── files.pug
│ │ ├── addconfirm.pug
│ │ ├── breadcrumb.pug
│ │ ├── selector.pug
│ │ └── accounts.pug
│ ├── js
│ │ ├── config.json
│ │ ├── config_prod.json
│ │ ├── files.js
│ │ ├── dropdown.js
│ │ ├── models
│ │ │ └── search.js
│ │ ├── breadcrumb.js
│ │ ├── iexd-transport.js
│ │ ├── izitoast-helper.js
│ │ ├── router-helper.js
│ │ ├── constants.js
│ │ ├── accounts.js
│ │ └── storage.js
│ └── localization
│ │ └── messages
│ │ ├── zh-CN.json
│ │ ├── zh-TW.json
│ │ ├── zh.json
│ │ ├── ko.json
│ │ ├── ja.json
│ │ ├── he.json
│ │ ├── fi.json
│ │ ├── th.json
│ │ ├── et.json
│ │ ├── cs.json
│ │ ├── sv.json
│ │ ├── da.json
│ │ ├── tr.json
│ │ ├── pl.json
│ │ ├── pt.json
│ │ ├── id.json
│ │ ├── nl.json
│ │ ├── it.json
│ │ ├── ro.json
│ │ ├── es.json
│ │ ├── ru.json
│ │ ├── fr.json
│ │ └── de.json
├── loader
│ ├── js
│ │ ├── vue
│ │ │ ├── index.js
│ │ │ ├── DefaultButtons.js
│ │ │ ├── Dropzone.js
│ │ │ └── creators.js
│ │ └── react
│ │ │ ├── index.js
│ │ │ ├── constants.js
│ │ │ └── Dropzone.jsx
│ └── css
│ │ └── modal.less
└── constants.js
├── .browserslistrc
├── storybook-common
├── README.footer.md
├── preview-head.html
└── webpack-config-generator.js
├── dev-server
├── static
│ ├── style.css
│ └── translations-suite-sample.json
├── ssl-cert.js
├── picker-template-hot-loader.js
├── dev-server.js
├── cert.pem
├── index.ejs
└── key.pem
├── .eslintignore
├── config
├── merge-strategy.js
├── common.js
├── loader-export-helper.js
├── build.js
├── story-dev-server.js
├── webpack.dev.conf.js
├── picker-plugins.js
├── webpack.base.conf.js
└── webpack.story.conf.js
├── .bowerrc
├── bower.json
├── test
├── ts
│ └── test_global.ts
└── dist-test-server.js
├── .eslintrc-ts.js
├── generate_npmignore.sh
├── template
└── picker.ejs
├── .gitignore
├── LICENSE.MIT
├── .eslintrc.js
├── CONTRIBUTORS.md
├── babel.config.js
└── stylelint.config.js
/.stylelintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook-vue/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../babel.config');
2 |
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kloudless/file-picker/HEAD/img/demo.gif
--------------------------------------------------------------------------------
/storybook-react/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../babel.config');
2 |
--------------------------------------------------------------------------------
/storybook-vue/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 | ../../storybook-common/preview-head.html
--------------------------------------------------------------------------------
/storybook-react/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 | ../../storybook-common/preview-head.html
--------------------------------------------------------------------------------
/vue.js:
--------------------------------------------------------------------------------
1 | // Re-export Vue bindings
2 | module.exports = require('./dist/commonjs2/vue.min.js');
3 |
--------------------------------------------------------------------------------
/react.js:
--------------------------------------------------------------------------------
1 | // Re-export dist/loader/js/react
2 | module.exports = require('./dist/commonjs2/react.min.js');
3 |
--------------------------------------------------------------------------------
/storybook-test/workflow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kloudless/file-picker/HEAD/storybook-test/workflow.png
--------------------------------------------------------------------------------
/src/picker/css/constants.less:
--------------------------------------------------------------------------------
1 | /*
2 | * Breaking Point
3 | */
4 | @BREAKING_POINT: 480px;
5 | @MODAL_WIDTH: 640px;
6 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | firefox > 56
2 | chrome > 56
3 | ie >= 11
4 | edge >= 12
5 | opera >= 49
6 | safari >= 8
7 | ios_saf >= 8
8 |
--------------------------------------------------------------------------------
/src/picker/templates/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Kloudless File Picker
5 | body
6 | include picker
7 |
--------------------------------------------------------------------------------
/storybook-common/README.footer.md:
--------------------------------------------------------------------------------
1 | - - -
2 | - Github: https://github.com/kloudless/file-picker
3 | - Support: support@kloudless.com
4 |
--------------------------------------------------------------------------------
/dev-server/static/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 | a {
6 | color: #00b7ff;
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .tmp
3 | dist
4 | lib
5 | dev-server/localization
6 | !.eslintrc.js
7 | !.eslintrc-ts.js
8 | storybook-test/static/style/material.min.js
9 |
--------------------------------------------------------------------------------
/src/picker/js/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "debug": true,
3 | "logLevel": 1,
4 | "static_path": "https://s3-us-west-2.amazonaws.com/static-assets.kloudless.com"
5 | }
6 |
--------------------------------------------------------------------------------
/src/picker/js/config_prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "debug": false,
3 | "logLevel": 3,
4 | "static_path": "https://s3-us-west-2.amazonaws.com/static-assets.kloudless.com"
5 | }
6 |
--------------------------------------------------------------------------------
/config/merge-strategy.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 |
3 | module.exports = merge.strategy(
4 | {
5 | 'module.rules': 'append',
6 | plugins: 'append',
7 | },
8 | );
9 |
--------------------------------------------------------------------------------
/storybook-react/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpackConfigGenerator = require(
2 | '../../storybook-common/webpack-config-generator');
3 |
4 | module.exports = webpackConfigGenerator(__dirname);
--------------------------------------------------------------------------------
/storybook-vue/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpackConfigGenerator = require(
2 | '../../storybook-common/webpack-config-generator');
3 |
4 | module.exports = webpackConfigGenerator(__dirname);
--------------------------------------------------------------------------------
/storybook-vue/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register';
2 | import '@storybook/addon-knobs/register';
3 | import '@storybook/addon-links/register';
4 | import 'storybook-readme/register';
--------------------------------------------------------------------------------
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "scripts": {
4 | "postinstall": "node ./node_modules/cldr-data-downloader/bin/download.js -i bower_components/cldr-data/index.json -o bower_components/cldr-data/"
5 | }
6 | }
--------------------------------------------------------------------------------
/src/picker/templates/dropzone.pug:
--------------------------------------------------------------------------------
1 | div#dropzone(style='background-color:#F5F6F7; color:#474747; width:100%; height:100%; text-align:center; cursor: pointer')
2 | span(style='position:relative; top:33%', data-bind='translate: "dropzone/message"')
3 |
4 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/__image_snapshots__/desktop-test-js-desktop-image-tests-file-view-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kloudless/file-picker/HEAD/storybook-test/tests/image/__image_snapshots__/desktop-test-js-desktop-image-tests-file-view-1-snap.png
--------------------------------------------------------------------------------
/storybook-test/tests/image/__image_snapshots__/mobile-test-js-mobile-image-tests-file-view-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kloudless/file-picker/HEAD/storybook-test/tests/image/__image_snapshots__/mobile-test-js-mobile-image-tests-file-view-1-snap.png
--------------------------------------------------------------------------------
/src/picker/css/dropdown.less:
--------------------------------------------------------------------------------
1 | .dropdown {
2 | cursor: default;
3 | position: absolute;
4 | top: 100%;
5 | right: 0;
6 | left: 0;
7 | display: none;
8 | margin-top: 5px;
9 | z-index: 2000;
10 | }
11 |
12 | .dropdown--toggled {
13 | display: block;
14 | }
15 |
--------------------------------------------------------------------------------
/storybook-react/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-options/register';
2 | // Temporarily disable this addon due to the issue:
3 | // https://github.com/storybookjs/storybook/issues/7215
4 | // import '@storybook/addon-actions/register';
5 | import '@storybook/addon-knobs/register';
6 | import 'storybook-readme/register';
7 |
--------------------------------------------------------------------------------
/dev-server/ssl-cert.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | let privateKey;
4 | let certificate;
5 |
6 | if (process.env.SSL_CERT) {
7 | privateKey = fs.readFileSync(process.env.SSL_KEY, 'utf8');
8 | certificate = fs.readFileSync(process.env.SSL_CERT, 'utf8');
9 | }
10 |
11 | module.exports = {
12 | privateKey,
13 | certificate,
14 | };
15 |
--------------------------------------------------------------------------------
/src/picker/templates/footer.pug:
--------------------------------------------------------------------------------
1 | .container__footer
2 | // ko ifnot: logo_url()
3 | .flex-row.justify-content-center
4 | .icon.icon--xsmall
5 | .icon__brand
6 | |
7 | a.container__footer-link(
8 | href="https://kloudless.com", target="_blank",
9 | data-bind='translate: {html: { message: "global/poweredby" }}')
10 | // /ko
11 |
--------------------------------------------------------------------------------
/src/picker/css/files.less:
--------------------------------------------------------------------------------
1 | .search-input {
2 | padding: 0 12px;
3 | width: 100%;
4 | height: 42px;
5 | background-color: @input_color;
6 | border: solid 1px @input_border_color;
7 | border-radius: @border_radius;
8 | }
9 |
10 | .account-name {
11 | .text-ellipsis();
12 |
13 | margin-left: 8px;
14 | height: 24px;
15 | line-height: 24px;
16 | font-size: @font_size_lg;
17 | }
18 |
--------------------------------------------------------------------------------
/src/picker/css/mkdir-form.less:
--------------------------------------------------------------------------------
1 | .mkdir-form {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | .mkdir-form__input {
7 | padding: 0 12px;
8 | width: 100%;
9 | height: 32px;
10 | background-color: @input_color;
11 | border: 1px solid @input_border_color;
12 | border-radius: @border_radius;
13 | }
14 |
15 | .mkdir-form__button {
16 | .btn();
17 | .btn--primary();
18 |
19 | width: unset;
20 | height: 32px;
21 | }
22 |
--------------------------------------------------------------------------------
/storybook-test/tests/setupTests.js:
--------------------------------------------------------------------------------
1 | const { configureToMatchImageSnapshot } = require('jest-image-snapshot');
2 |
3 | const toMatchImageSnapshot = configureToMatchImageSnapshot({
4 | // https://github.com/americanexpress/jest-image-snapshot#%EF%B8%8F-api
5 | comparisonMethod: 'pixelmatch',
6 | customDiffConfig: {
7 | threshold: 0.5,
8 | },
9 | blur: 1,
10 | });
11 | expect.extend({ toMatchImageSnapshot });
12 |
13 | jest.setTimeout(5 * 60 * 1000);
14 |
--------------------------------------------------------------------------------
/storybook-vue/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, addParameters, addDecorator } from '@storybook/vue';
2 | import { addReadme } from 'storybook-readme/vue';
3 |
4 | addParameters({
5 | options: {
6 | name: 'Kloudless File Picker',
7 | addonPanelInRight: false,
8 | showStoriesPanel: true
9 | }
10 | });
11 |
12 | addDecorator(addReadme);
13 |
14 | function loadStories() {
15 | require('../stories/index.js');
16 | }
17 |
18 | configure(loadStories, module);
19 |
--------------------------------------------------------------------------------
/storybook-react/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure, addParameters, addDecorator } from '@storybook/react';
2 | import '@storybook/addon-console';
3 | import { addReadme } from 'storybook-readme';
4 |
5 | addParameters({
6 | options: {
7 | name: 'Kloudless File Picker',
8 | addonPanelInRight: false,
9 | showStoriesPanel: true
10 | }
11 | });
12 | addDecorator(addReadme);
13 |
14 | function loadStories() {
15 | require('../stories/index.jsx');
16 | }
17 |
18 | configure(loadStories, module);
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kloudless-file-picker",
3 | "version": "1.0.0",
4 | "private": false,
5 | "dependencies": {
6 | "jquery-ui": "1.10.4",
7 | "requirejs": "2.1.14",
8 | "loglevel": "1.4.1",
9 | "momentjs": "2.7.0",
10 | "almond": "0.2.9",
11 | "jquery-dropdown": "1.0.6",
12 | "jquery-scrollstop": "1.1.0",
13 | "cldr-data": "29.0.0",
14 | "globalize": "1.3.0",
15 | "requirejs-plugins": "^1.0.3"
16 | },
17 | "resolutions": {
18 | "jquery": "2.1.3"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/storybook-test/tests/globalTeardown.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line max-len
2 | // https://github.com/smooth-code/jest-puppeteer#create-your-own-globalsetup-and-globalteardown
3 | const { teardown: teardownPuppeteer } = require('jest-environment-puppeteer');
4 | const { teardown: teardownDevServer } = require('jest-dev-server');
5 |
6 |
7 | module.exports = async function globalTeardown(globalConfig) {
8 | await teardownPuppeteer(globalConfig);
9 | if (process.env.CI) {
10 | await teardownDevServer();
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/desktop.test.js:
--------------------------------------------------------------------------------
1 | import PuppeteerHelper from './core/PuppeteerHelper';
2 | import { STORY_URL } from '../../config';
3 |
4 | describe('Desktop Image Tests', () => {
5 | const helper = new PuppeteerHelper();
6 |
7 | beforeEach(async () => {
8 | await helper.init(STORY_URL.chooser);
9 | });
10 |
11 | afterEach(async () => {
12 | await helper.cleanup();
13 | });
14 |
15 | it('file view', async () => {
16 | await helper.launch();
17 | await helper.assertScreenshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/mobile.test.js:
--------------------------------------------------------------------------------
1 | import PuppeteerHelper from './core/PuppeteerHelper';
2 | import { STORY_URL } from '../../config';
3 |
4 | describe('Mobile Image Tests', () => {
5 | const helper = new PuppeteerHelper();
6 |
7 | beforeEach(async () => {
8 | await helper.init(STORY_URL.chooser, { mobile: true });
9 | });
10 |
11 | afterEach(async () => {
12 | await helper.cleanup();
13 | });
14 |
15 | it('file view', async () => {
16 | await helper.launch();
17 | await helper.assertScreenshot();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/storybook-test/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current',
8 | },
9 | },
10 | ],
11 | ],
12 | // We have config.js which is used by jest and storybook.
13 | // Jest globalSetup.js and testEnvironment.js only accepts CommonJS while
14 | // storybook uses ES6 module syntax.
15 | // This plugin helps resolve the problems of mixing CommonJS and ES6 module.
16 | plugins: ['@babel/plugin-transform-modules-commonjs'],
17 | };
18 |
--------------------------------------------------------------------------------
/src/picker/templates/picker.pug:
--------------------------------------------------------------------------------
1 | div#kloudless-file-picker
2 | div.h-100.w-100(data-bind="template: {name: current}")
3 |
4 | // views
5 | script(type='text/html', id='accounts')
6 | include accounts
7 | script(type='text/html', id='files')
8 | include files
9 | script(type='text/html', id='computer')
10 | include computer
11 | script(type='text/html', id='addConfirm')
12 | include addconfirm
13 | script(type='text/html', id='dropzone')
14 | include dropzone
15 | script(type='text/html', id='search')
16 | include search
17 |
18 |
19 |
--------------------------------------------------------------------------------
/storybook-test/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/storybook-test/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'eslint:recommended',
4 | 'plugin:react/recommended',
5 | ],
6 | env: {
7 | jest: true,
8 | },
9 | globals: {
10 | // Jest Puppeteer exposes three globals: browser, page, context
11 | page: 'readonly',
12 | browser: 'readonly',
13 | context: 'readonly',
14 | jestPuppeteer: 'readonly',
15 | },
16 | rules: {
17 | 'no-console': 'off',
18 | 'react/prop-types': 'off',
19 | },
20 | settings: {
21 | react: {
22 | version: 'detect',
23 | },
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/picker/templates/mkdir-form.pug:
--------------------------------------------------------------------------------
1 | form.mkdir-form(data-bind="submit: mkdir")
2 | .flex-row.align-items-center
3 | .flex-grow.mr
4 | input.mkdir-form__input(required, data-bind=`
5 | selectInputText: "Untitled Folder",
6 | translate: {
7 | value: { message: "files/untitledFolder" },
8 | placeholder: { message: "files/folderName" }}`)
9 | .mr
10 | button.mkdir-form__button(
11 | data-bind='translate: "global/save"', type="submit")
12 | .flex-no-shrink
13 | .icon(data-bind='click: rmdir')
14 | .icon__close
--------------------------------------------------------------------------------
/src/loader/js/vue/index.js:
--------------------------------------------------------------------------------
1 | import { createChooser, createSaver } from './creators';
2 | import Dropzone from './Dropzone';
3 | import filePicker from '../interface';
4 |
5 | const Chooser = createChooser();
6 | const Saver = createSaver();
7 |
8 | // named export
9 | export {
10 | createChooser, createSaver, Chooser, Saver, Dropzone,
11 | };
12 |
13 | export const { getGlobalOptions, setGlobalOptions } = filePicker;
14 |
15 | // default export
16 | export default {
17 | createChooser,
18 | createSaver,
19 | Chooser,
20 | Saver,
21 | Dropzone,
22 | getGlobalOptions,
23 | setGlobalOptions,
24 | };
25 |
--------------------------------------------------------------------------------
/test/ts/test_global.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, max-len */
2 |
3 | // eslint-disable-next-line spaced-comment, @typescript-eslint/triple-slash-reference
4 | ///
5 |
6 | const options: Kloudless.filePicker.ChooserOptions = {
7 | app_id: 'APP_ID',
8 | types: ['files'],
9 | };
10 | const picker = Kloudless.filePicker.picker(options);
11 | picker.choosify(document.getElementById('button'));
12 | picker.on('success', (files: Kloudless.filePicker.FileMetadata[]) => {
13 | files.forEach((file) => {
14 | console.log(file.id, file.name);
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/picker/css/box.less:
--------------------------------------------------------------------------------
1 | .box {
2 | width: 100%;
3 | color: @primary_text_color;
4 | background-color: @section_primary_bg_color;
5 | border: solid 1px @section_line_color;
6 | border-radius: @border_radius;
7 | }
8 |
9 | .box--shadow {
10 | box-shadow: 0 1px 2px 1px @shadow_color;
11 | }
12 |
13 | .box__title {
14 | padding: 16px 16px 0 16px;
15 | font-size: @font_size_sm;
16 | color: @secondary_text_color;
17 | text-transform: uppercase;
18 | }
19 |
20 | .box__section {
21 | padding: 8px 12px;
22 | }
23 |
24 | .box__divider {
25 | margin: 0;
26 | border-top: 1px solid @section_line_color;
27 | }
28 |
--------------------------------------------------------------------------------
/storybook-test/.storybook/main.js:
--------------------------------------------------------------------------------
1 | const { devServerPorts } = require('../../config/common');
2 |
3 | // Prefix with `STORYBOOK_` so that they can be accessed in stories.
4 | [
5 | 'PICKER_URL', 'BASE_URL', 'KLOUDLESS_ACCOUNT_TOKEN',
6 | 'KLOUDLESS_APP_ID', 'LOADER_PATH',
7 | ].forEach(key=>{
8 | process.env[`STORYBOOK_${key}`] = process.env[key] || '';
9 | });
10 |
11 | process.env.STORYBOOK_LOADER_PATH = (
12 | process.env.LOADER_PATH || `http://localhost:${devServerPorts.loader}/sdk`
13 | );
14 |
15 | module.exports = {
16 | stories: ['../stories/**/*.stories.js'],
17 | addons: ['@storybook/addon-actions'],
18 | };
19 |
--------------------------------------------------------------------------------
/storybook-test/stories/basic.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStory, createDropzoneStory } from './core';
3 |
4 | const { filePickerReact } = window.Kloudless;
5 |
6 | export default {
7 | title: 'E2E Test',
8 | };
9 |
10 | const ChooserStory = createStory(filePickerReact.Chooser);
11 | const SaverStory = createStory(filePickerReact.Saver);
12 | const DropzoneStory = createDropzoneStory(filePickerReact.Dropzone);
13 |
14 | export const Chooser = () => ;
15 | export const Saver = () => ;
16 | export const Dropzone = () => ;
17 |
--------------------------------------------------------------------------------
/src/loader/js/react/index.js:
--------------------------------------------------------------------------------
1 | import { createChooser, createSaver } from './creators';
2 | import Dropzone from './Dropzone';
3 | import filePicker from '../interface';
4 |
5 | const Saver = createSaver();
6 | const Chooser = createChooser();
7 |
8 | // named exports
9 | export {
10 | createChooser,
11 | createSaver,
12 | Saver,
13 | Chooser,
14 | Dropzone,
15 | };
16 |
17 | export const { setGlobalOptions, getGlobalOptions } = filePicker;
18 |
19 | // default exports
20 | export default {
21 | createChooser,
22 | createSaver,
23 | Saver,
24 | Chooser,
25 | Dropzone,
26 | setGlobalOptions,
27 | getGlobalOptions,
28 | };
29 |
--------------------------------------------------------------------------------
/storybook-test/tests/integration/core/ApiHelper.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | class ApiHelper {
4 | constructor(baseUrl, token) {
5 | this.axiosInstance = axios.create({
6 | baseURL: `${baseUrl}/v1/accounts/me/`,
7 | headers: {
8 | Authorization: `Bearer ${token}`,
9 | },
10 | });
11 | }
12 |
13 | async listFolderContent(folderId, options = { }) {
14 | const { page = '', pageSize = 1000 } = options;
15 | return this.axiosInstance.get(
16 | `storage/folders/${folderId}/contents?page=${page}&page_size=${pageSize}`,
17 | );
18 | }
19 | }
20 |
21 | module.exports = ApiHelper;
22 |
--------------------------------------------------------------------------------
/src/picker/css/list.less:
--------------------------------------------------------------------------------
1 | .list {
2 | background-color: @section_primary_bg_color;
3 | }
4 |
5 | .list__item {
6 | display: flex;
7 | align-items: center;
8 | cursor: pointer;
9 | padding: 10px 4px;
10 | color: @primary_text_color;
11 |
12 | &:hover {
13 | background-color: @section_primary_hover_bg_color;
14 | }
15 | }
16 |
17 | .list__item--current {
18 | cursor: default;
19 | justify-content: space-between;
20 | pointer-events: none;
21 |
22 | &:hover {
23 | background-color: @section_primary_bg_color;
24 | }
25 | }
26 |
27 | .list__text {
28 | .word-break();
29 |
30 | margin: 0 10px;
31 | flex: 1;
32 | }
33 |
--------------------------------------------------------------------------------
/src/picker/templates/computer.pug:
--------------------------------------------------------------------------------
1 | .container
2 | .container__header
3 | .flex-row.align-items-center
4 | .flex-no-shrink.mr.sm-hidden
5 | //- an empty icon
6 | .icon.icon--large
7 | .flex-grow
8 | include selector
9 | .flex-no-shrink.ml(data-bind="css: {'sm-hidden': attachMode()}")
10 | #plupload_btn_cancel.icon.icon--large(
11 | data-bind="css: {'invisible': attachMode()}")
12 | .icon__close(data-bind='css: $root.e2eSelectors.J_CLOSE_BTN')
13 | .container__body
14 | #computer_uploader.h-100
15 | p(data-bind='translate: "computer/noSupport"')
16 | include footer
17 |
--------------------------------------------------------------------------------
/storybook-test/static/style/icon.css:
--------------------------------------------------------------------------------
1 | /* Copy from https://fonts.googleapis.com/icon?family=Material+Icons */
2 |
3 | @font-face {
4 | font-family: 'Material Icons';
5 | font-style: normal;
6 | font-weight: 400;
7 | src: url(https://fonts.gstatic.com/s/materialicons/v53/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf) format('truetype');
8 | }
9 |
10 | .material-icons {
11 | font-family: 'Material Icons';
12 | font-weight: normal;
13 | font-style: normal;
14 | font-size: 24px;
15 | line-height: 1;
16 | letter-spacing: normal;
17 | text-transform: none;
18 | display: inline-block;
19 | white-space: nowrap;
20 | word-wrap: normal;
21 | direction: ltr;
22 | }
23 |
--------------------------------------------------------------------------------
/.eslintrc-ts.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Based on the .eslintrc.js.
3 | * All the configurations remain the same besides @typescript-eslint related
4 | * ones.
5 | */
6 |
7 | const config = require('./.eslintrc.js');
8 |
9 | config.globals.Kloudless = 'readonly';
10 |
11 | config.extends = [
12 | 'airbnb-base',
13 | 'plugin:@typescript-eslint/recommended',
14 | ];
15 | config.parser = '@typescript-eslint/parser';
16 | config.plugins = ['@typescript-eslint'];
17 |
18 | config.rules['@typescript-eslint/camelcase'] = 'off';
19 | // disable import/named to avoid un-expected errors that actually work
20 | // fine with typescript
21 | config.rules['import/named'] = 'off';
22 |
23 | module.exports = config;
24 |
--------------------------------------------------------------------------------
/src/loader/js/vue/DefaultButtons.js:
--------------------------------------------------------------------------------
1 | const BaseButton = {
2 | functional: true,
3 | render(createElement, context) {
4 | return createElement('button', {
5 | ...context.data, // passing attributes and event handlers
6 | domProps: {
7 | textContent: context.props.title,
8 | },
9 | });
10 | },
11 | };
12 |
13 | export const ChooserButton = {
14 | ...BaseButton,
15 | name: 'Chooser',
16 | props: {
17 | title: {
18 | type: String,
19 | default: 'Choose a file',
20 | },
21 | },
22 | };
23 |
24 | export const SaverButton = {
25 | ...BaseButton,
26 | name: 'Saver',
27 | props: {
28 | title: {
29 | type: String,
30 | default: 'Save a file',
31 | },
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/src/picker/css/index.less:
--------------------------------------------------------------------------------
1 | @import './constants.less';
2 | @import './variables.less';
3 |
4 | // mixins
5 | @import './global.less';
6 | @import './mixins.less';
7 | @import './container.less';
8 | @import './util.less';
9 |
10 | // general component
11 | @import './icons.less';
12 | @import './buttons.less';
13 | @import './box.less';
14 | @import './list.less';
15 | @import './dropdown.less';
16 | @import './breadcrumb.less';
17 | @import './selector.less';
18 | @import './mkdir-form.less';
19 | @import './filetable.less';
20 |
21 | // views
22 | @import './accounts.less';
23 | @import './files.less';
24 | @import './computer.less';
25 |
26 | //material-icons-font
27 | @import './material-icons-font.less';
28 |
29 | // iziToast
30 | @import './izitoast.less';
31 |
--------------------------------------------------------------------------------
/src/loader/js/react/constants.js:
--------------------------------------------------------------------------------
1 | export const EVENT_HANDLER_MAPPING = {
2 | success: 'onSuccess',
3 | cancel: 'onCancel',
4 | error: 'onError',
5 | open: 'onOpen',
6 | load: 'onLoad',
7 | close: 'onClose',
8 | selected: 'onSelected',
9 | addAccount: 'onAddAccount',
10 | deleteAccount: 'onDeleteAccount',
11 | startFileUpload: 'onStartFileUpload',
12 | finishFileUpload: 'onFinishFileUpload',
13 | logout: 'onLogout',
14 | };
15 |
16 | export const DROPZONE_EVENT_HANDLER_MAPPING = {
17 | ...EVENT_HANDLER_MAPPING,
18 | dropzoneClicked: 'onClick',
19 | drop: 'onDrop',
20 | };
21 |
22 | export const EVENT_HANDLERS = Object.values(EVENT_HANDLER_MAPPING);
23 |
24 | export const DROPZONE_EVENT_HANDLERS = Object.values(
25 | DROPZONE_EVENT_HANDLER_MAPPING,
26 | );
27 |
--------------------------------------------------------------------------------
/storybook-common/preview-head.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/core/get_account_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 1234567890,
3 | "account": "this-is-a-very-loooooooooooooooooooong-account@kloudless.com",
4 | "service": "dropbox",
5 | "internal_use": false,
6 | "created": "2020-05-15T03:24:56.887056Z",
7 | "modified": "2020-10-07T10:29:23.282659Z",
8 | "service_name": "Dropbox",
9 | "admin": false,
10 | "apis": [
11 | "sharing",
12 | "events",
13 | "storage"
14 | ],
15 | "effective_scope": "gdrive:normal.events.default:kloudless gdrive:normal.storage.default:kloudless gdrive:normal.sharing.default:kloudless",
16 | "api": "core",
17 | "type": "account",
18 | "enabled": true,
19 | "object_definitions": {},
20 | "custom_properties": {},
21 | "proxy_connection": null,
22 | "active": true
23 | }
24 |
--------------------------------------------------------------------------------
/generate_npmignore.sh:
--------------------------------------------------------------------------------
1 | # create .npmignore base on .gitignore
2 | cp .gitignore .npmignore
3 | # whitelist .gitignore and dist folder in .npmignore
4 | # so that npm will pick these two when packing
5 | echo '# generate by generate_npmignore.sh' >> .npmignore
6 | echo 'storybook-common/' >> .npmignore
7 | echo 'storybook-react/' >> .npmignore
8 | echo 'storybook-vue/' >> .npmignore
9 | echo 'storybook-test/' >> .npmignore
10 | echo '.eslintrc.js' >> .npmignore
11 | echo '.eslintignore' >> .npmignore
12 | echo '.bowerrc' >> .npmignore
13 | echo 'bower.json' >> .npmignore
14 | echo 'webpack.loader.config.js' >> .npmignore
15 | echo 'Gruntfile.js' >> .npmignore
16 | echo '!.gitignore' >> .npmignore
17 | echo '!dist/commonjs2' >> .npmignore
18 | echo '!react.js' >> .npmignore
19 | echo '!vue.js' >> .npmignore
20 |
--------------------------------------------------------------------------------
/template/picker.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Kloudless File Picker
5 | <%# Only change this line if you are moving picker.css to a different
6 | location. %>
7 |
8 |
9 |
10 |
11 | <%#
12 | This line includes html elements for the File Picker. You can move it
13 | around as long as it is inside body tag.
14 | %>
15 | <% include ../dist/template/index.html %>
16 |
17 | <%# Only change this line if you are moving picker.js to a different
18 | location. DO NOT remove or change the "id" attribute %>
19 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/picker/css/selector.less:
--------------------------------------------------------------------------------
1 | .account-selector .dropdown .box {
2 | overflow-y: auto;
3 | max-height: 50vh;
4 | }
5 |
6 | .account-selector {
7 | cursor: pointer;
8 | position: relative;
9 | width: 100%;
10 | text-align: left;
11 | }
12 |
13 | .account-selector__button {
14 | display: flex;
15 | align-items: center;
16 | padding: 10px 12px;
17 | width: 100%;
18 | color: @primary_text_color;
19 | background-color: @input_color;
20 | border: solid 1px @input_border_color;
21 | border-radius: @border_radius;
22 | }
23 |
24 | .account-selector__name {
25 | .word-break();
26 |
27 | margin: 0 8px;
28 | flex: 1;
29 | }
30 |
31 | .account-selector__arrow {
32 | width: 0;
33 | height: 0;
34 | border-left: 4px solid transparent;
35 | border-right: 4px solid transparent;
36 | border-top: 4px solid #aaa;
37 | }
38 |
--------------------------------------------------------------------------------
/storybook-test/jest-puppeteer.config.js:
--------------------------------------------------------------------------------
1 | const DEBUG = Boolean(JSON.parse(process.env.DEBUG || false));
2 |
3 | module.exports = {
4 | launch: {
5 | headless: !DEBUG,
6 | devtools: DEBUG,
7 | // Whether to pipe the browser process stdout and stderr into process.stdout
8 | // and process.stderr.
9 | dumpio: false,
10 | args: [
11 | '--no-sandbox',
12 | '--disable-setuid-sandbox',
13 | '--disable-dev-shm-usage',
14 | '--auto-open-devtools-for-tabs',
15 | // Need this to access iframe when turning off headless.
16 | // REF: https://github.com/puppeteer/puppeteer/issues/4960
17 | '--disable-features=site-per-process',
18 | ],
19 | ignoreDefaultArgs: ['--hide-scrollbars'],
20 | timeout: 60000, // timeout for launching browser
21 | },
22 | browser: 'chromium',
23 | browserContext: 'default',
24 | };
25 |
--------------------------------------------------------------------------------
/storybook-test/tests/testEnvironment.js:
--------------------------------------------------------------------------------
1 | // https://github.com/smooth-code/jest-puppeteer#extend-puppeteerenvironment
2 | const fs = require('fs');
3 | const os = require('os');
4 | const path = require('path');
5 | const PuppeteerEnvironment = require('jest-environment-puppeteer');
6 | const { TEST_DATA } = require('../config');
7 |
8 | class FilePickerTestEnvironment extends PuppeteerEnvironment {
9 | // Execute once in each worker.
10 | async setup() {
11 | await super.setup();
12 |
13 | // Read test data from file.
14 | const filepath = path.resolve(os.homedir(), TEST_DATA);
15 | let data = fs.readFileSync(filepath, { encoding: 'utf-8' });
16 | data = JSON.parse(data);
17 | Object.keys(data).forEach((key) => {
18 | this.global[key] = data[key];
19 | });
20 | }
21 |
22 | async teardown() {
23 | await super.teardown();
24 | }
25 | }
26 |
27 | module.exports = FilePickerTestEnvironment;
28 |
--------------------------------------------------------------------------------
/storybook-common/webpack-config-generator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Generate webpack configuration for Storybook React and Storybook Vue.
3 | */
4 |
5 | const path = require('path');
6 | const AutoPrefixer = require('autoprefixer');
7 |
8 | module.exports = basePath => async ({ config }) => {
9 | config.module.rules.push(
10 | {
11 | test: /\.jsx?$/,
12 | include: [
13 | path.resolve(basePath, '..', '..', 'src', 'loader'),
14 | path.resolve(basePath, '..', 'stories'),
15 | ],
16 | exclude: /node_modules/,
17 | use: {
18 | loader: 'babel-loader',
19 | },
20 | },
21 | {
22 | test: /\.less$/,
23 | use: [
24 | 'style-loader',
25 | 'css-loader',
26 | {
27 | loader: 'postcss-loader',
28 | options: { plugins: [AutoPrefixer()] },
29 | },
30 | 'less-loader',
31 | ],
32 | },
33 | );
34 |
35 | return config;
36 | };
37 |
--------------------------------------------------------------------------------
/storybook-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook-react",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "",
6 | "scripts": {
7 | "storybook": "start-storybook -p 9001",
8 | "build": "build-storybook -c .storybook -o ../.demo/react"
9 | },
10 | "author": "Kloudless (https://kloudless.com)",
11 | "license": "MIT",
12 | "devDependencies": {
13 | "@babel/core": "7.3.4",
14 | "@babel/preset-env": "7.4.1",
15 | "@storybook/addon-actions": "5.1.9",
16 | "@storybook/addon-console": "1.1.0",
17 | "@storybook/addon-info": "5.1.9",
18 | "@storybook/addon-knobs": "5.1.9",
19 | "@storybook/addon-options": "5.1.9",
20 | "@storybook/react": "5.1.9",
21 | "babel-loader": "8.0.5",
22 | "babel-plugin-transform-define": "1.3.1",
23 | "core-js": "3.1.4",
24 | "react": "16.8.4",
25 | "react-dom": "16.8.4",
26 | "storybook-readme": "5.0.2"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/storybook-test/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "storybook": "start-storybook -p 9001 -s ./static --ci",
7 | "build-storybook": "build-storybook",
8 | "test": "jest",
9 | "dev-server": "npm run dev:story --prefix=../"
10 | },
11 | "author": "",
12 | "license": "MIT",
13 | "dependencies": {},
14 | "devDependencies": {
15 | "@babel/plugin-transform-modules-commonjs": "7.12.1",
16 | "@storybook/addon-actions": "5.3.18",
17 | "@storybook/addons": "5.3.18",
18 | "@storybook/react": "5.3.18",
19 | "axios": "0.20.0",
20 | "babel-jest": "26.5.2",
21 | "eslint": "7.13.0",
22 | "eslint-plugin-react": "7.21.5",
23 | "expect-puppeteer": "4.4.0",
24 | "jest": "26.5.2",
25 | "jest-dev-server": "4.4.0",
26 | "jest-image-snapshot": "4.2.0",
27 | "jest-puppeteer": "4.4.0",
28 | "puppeteer": "5.3.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/storybook-vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook-vue",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "",
6 | "author": "Kloudless (https://kloudless.com)",
7 | "license": "MIT",
8 | "devDependencies": {
9 | "@babel/core": "7.4.0",
10 | "@babel/preset-env": "7.4.1",
11 | "@storybook/addon-actions": "5.1.9",
12 | "@storybook/addon-knobs": "5.1.9",
13 | "@storybook/addon-links": "5.1.9",
14 | "@storybook/addons": "5.1.9",
15 | "@storybook/vue": "5.1.9",
16 | "babel-loader": "8.0.5",
17 | "babel-plugin-stylus-compiler": "1.4.0",
18 | "babel-preset-vue": "2.0.2",
19 | "core-js": "3.1.4",
20 | "storybook-readme": "5.0.2",
21 | "vue": "2.6.10",
22 | "vue-loader": "15.7.0",
23 | "vue-template-compiler": "2.6.10"
24 | },
25 | "scripts": {
26 | "storybook": "start-storybook -p 9001",
27 | "build": "build-storybook -c .storybook -o ../.demo/vue"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/storybook-test/tests/integration/index.test.js:
--------------------------------------------------------------------------------
1 | import PuppeteerHelper from './core/PuppeteerHelper';
2 | import { STORY_URL } from '../../config';
3 |
4 | describe('Chooser Tests', () => {
5 | const helper = new PuppeteerHelper();
6 |
7 | beforeEach(async () => {
8 | await helper.init(STORY_URL.chooser);
9 | });
10 |
11 | afterEach(async () => {
12 | await helper.cleanup();
13 | });
14 |
15 | it('select a file', async () => {
16 | await helper.launch();
17 | const selectedFile = global.FOLDER_CONTENT.find(
18 | f => f.type === 'file' && f.downloadable !== false,
19 | );
20 | if (!selectedFile) {
21 | throw new Error('No file to tests.');
22 | }
23 | await helper.clickFile(selectedFile.id);
24 | const [
25 | selectedEventData, successEventData,
26 | ] = await helper.clickSelectBtn();
27 |
28 | expect(selectedEventData).toEqual([selectedFile]);
29 | expect(successEventData).toEqual([selectedFile]);
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/src/picker/css/accounts.less:
--------------------------------------------------------------------------------
1 | .service-list {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding: 4px 0 4px 4px;
5 |
6 | @media only screen and (max-width: @BREAKING_POINT) {
7 | justify-content: center;
8 | }
9 | }
10 |
11 | .service-list__item {
12 | .shadow-box();
13 |
14 | flex: 0 0 120px;
15 | cursor: pointer;
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | justify-content: space-evenly;
20 | margin: 10px 16px 10px 0;
21 | padding: 6px;
22 | width: 120px;
23 | height: 120px;
24 | font-size: @font_size_lg;
25 | color: @primary_text_color;
26 |
27 | &:hover {
28 | background-color: @section_primary_hover_bg_color;
29 | }
30 | }
31 |
32 | .service-list__img {
33 | display: flex;
34 | justify-content: center;
35 | }
36 |
37 | .service-list__text {
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | width: 100%;
42 | word-break: break-word;
43 | text-align: center;
44 | }
45 |
--------------------------------------------------------------------------------
/src/picker/js/files.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import ko from 'knockout';
3 | import logger from 'loglevel';
4 |
5 | 'use strict';
6 |
7 | // TODO: better handling of file uploads?
8 | var FileManager = function () {
9 | this.files = ko.observableArray([]);
10 | this.current = ko.observable({});
11 | };
12 |
13 | // Add a file to upload.
14 | FileManager.prototype.add = function (url, name) {
15 | logger.debug('Adding file: ', url)
16 | this.files.push({
17 | url: url,
18 | name: name
19 | });
20 | };
21 |
22 | // Cancel current upload.
23 | FileManager.prototype.cancel = function () {
24 | this.files.remove(function (file) {
25 | return file.url == this.current().url;
26 | });
27 | };
28 |
29 | // Upload current file. Fire callback
30 | FileManager.prototype.upload = function (location_data, callbacks) {
31 | if (this.current()) {
32 | var file = this.current();
33 | logger.debug('Uploading current file: ', file.url);
34 | }
35 | };
36 |
37 | export default FileManager;
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Deployed apps should consider commenting this line out:
24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
25 | node_modules
26 | bower_components
27 |
28 | .tmp
29 | dist/*
30 | test/dist
31 | dev/
32 |
33 | # Mac
34 | .DS_Store
35 | .idea/
36 |
37 | # test
38 | test/package-lock.json
39 | storybook-test/static/loader/
40 | storybook-test/static/picker/
41 |
42 | # tools, IDE
43 | .nvmrc
44 | .vscode
45 | yarn-error.log
46 |
47 | # demo
48 | .demo
49 |
50 | .npmignore
51 | *.tgz
52 |
53 | .env
54 |
--------------------------------------------------------------------------------
/LICENSE.MIT:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Kloudless
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/config/common.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Export common variables used by webpack, babel, and jest configs
3 | */
4 |
5 | module.exports = {
6 | devServerPorts: {
7 | loader: 8081,
8 | picker: 8082,
9 | },
10 | /**
11 | * A list of paths in regexp format to specify which files / folders
12 | * babel should ignore.
13 | *
14 | * This list is used by
15 | * 1. 'ignore' option in babel.config.js
16 | * 2. 'transformIgnorePatterns' option in jest.conf.js
17 | */
18 | ignorePaths: [
19 | new RegExp('(bower_components)'),
20 | new RegExp(
21 | // eslint-disable-next-line max-len
22 | 'node_modules/@kloudless/file-picker-plupload-module/(?!(jquery.ui.plupload))',
23 | ),
24 | new RegExp('node_modules/(?!(@kloudless/file-picker-plupload-module))'),
25 | new RegExp('lib/(?!(jquery.ajax-retry))'),
26 | ],
27 | /**
28 | * A list of paths to resolve module imports
29 | *
30 | * This list is used by
31 | * 1. 'module-resolver' plugin's root option in babel.config.js
32 | * 2. webpack's resolve.modules option
33 | */
34 | resolvePaths: ['src', 'lib', 'node_modules', 'bower_components'],
35 | };
36 |
--------------------------------------------------------------------------------
/src/picker/css/mixins.less:
--------------------------------------------------------------------------------
1 | /**
2 | * @font-face LESS Mixin
3 | * use: .font-face(
4 | * @font-family, // name
5 | * @file-path, // absolute/relative URL to font files
6 | * @font-format, // font format. ex: woff
7 | * )
8 | */
9 | .font-face(@font-family, @file-path, @font-format) {
10 | @font-face {
11 | font-family: @font-family;
12 | src: url('@{file-path}') format('@{font-format}');
13 | font-weight: 500;
14 | font-style: normal;
15 | }
16 | }
17 |
18 | .shadow-box {
19 | border-radius: @border_radius;
20 | box-shadow: 0 1px 2px 1px @shadow_color;
21 | }
22 |
23 | .text-ellipsis {
24 | overflow: hidden;
25 | text-overflow: ellipsis;
26 | white-space: nowrap;
27 | }
28 |
29 | .hover-supported(@rules) {
30 | /*
31 | * https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer
32 | * coarse: The primary input mechanism includes a pointing device of limited accuracy.
33 | */
34 | @media not all and (pointer: coarse) {
35 | &:hover {
36 | @rules();
37 | }
38 | }
39 | }
40 |
41 | .word-break {
42 | // break-all is applied by IE and Edge
43 | word-break: break-all;
44 | word-break: break-word;
45 | }
46 |
--------------------------------------------------------------------------------
/src/picker/templates/search.pug:
--------------------------------------------------------------------------------
1 | .container(data-bind=`css: {
2 | 'container--is-loading': loading() || processingConfirm() }`)
3 | .container__header
4 | .flex-row.align-items-center
5 | .flex-no-shrink.mr
6 | .icon.icon--large(
7 | data-bind='click: files.toggleSearchView.bind($data, false)')
8 | .icon__back
9 | .flex-grow.mr
10 | form(data-bind="submit: files.doSearch")
11 | input.search-input(
12 | type="text", placeholder="Search",
13 | data-bind=`
14 | selectInputText: files.searchQuery,
15 | textInput: files.searchQuery,
16 | translate: {placeholder: { message: "files/search" }}`)
17 | .flex-no-shrink.invisible.sm-hidden
18 | .icon.icon--large
19 | .container__body
20 | .flex-col.flex-grow.h-100
21 | .flex-no-shrink.w-100
22 | .flex-row.mb
23 | .flex-no-shrink
24 | .icon
25 | img.icon__service(data-bind='attr: {src: accounts.active_logo()}')
26 | .flex-grow
27 | .account-name(data-bind='text: accounts.name')
28 | include filetable
29 | include footer
--------------------------------------------------------------------------------
/config/loader-export-helper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Export filePicker to the following global variables:
3 | * - window.Kloudless.filePicker
4 | * - window.Kloudless.fileExplorer (b/w compatible)
5 | * - window.Kloudless (b/w compatible)
6 | */
7 | import 'core-js/stable';
8 | import 'regenerator-runtime/runtime';
9 | import filePicker from '../src/loader/js/interface';
10 |
11 | // Determine export target
12 | let currentScript;
13 | if (document.currentScript) {
14 | currentScript = document.currentScript; // eslint-disable-line
15 | } else {
16 | const scripts = document.getElementsByTagName('script');
17 | currentScript = scripts[scripts.length - 1];
18 | }
19 |
20 | const customExportTarget = currentScript.getAttribute('data-kloudless-object');
21 | if (customExportTarget) {
22 | window[customExportTarget] = window[customExportTarget] || {};
23 | Object.assign(window[customExportTarget], filePicker);
24 | } else {
25 | window.Kloudless = window.Kloudless || {};
26 | // b/c with <=1.0.0
27 | Object.assign(window.Kloudless, filePicker);
28 | // b/c with ^1.0.1
29 | window.Kloudless.fileExplorer = filePicker;
30 |
31 | window.Kloudless.filePicker = filePicker;
32 | }
33 |
--------------------------------------------------------------------------------
/src/picker/css/breadcrumb.less:
--------------------------------------------------------------------------------
1 | .breadcrumb .dropdown .box {
2 | overflow-y: auto;
3 | max-height: 50vh;
4 | }
5 |
6 | .breadcrumb .icon {
7 | flex-shrink: 0;
8 | }
9 |
10 | .breadcrumb {
11 | position: relative;
12 | display: flex;
13 | flex-wrap: nowrap;
14 | align-items: center;
15 | width: 100%;
16 | font-size: @font_size_lg;
17 | }
18 |
19 | .breadcrumb__hidden {
20 | position: absolute;
21 | width: auto;
22 | height: auto;
23 | white-space: nowrap;
24 | visibility: hidden;
25 | }
26 |
27 | .breadcrumb__root-dir-text {
28 | margin-left: 8px;
29 | color: @primary_text_color;
30 | }
31 |
32 | .breadcrumb__toggle-btn {
33 | display: flex;
34 | align-items: center;
35 | flex-shrink: 0;
36 | white-space: nowrap;
37 | }
38 |
39 | .breadcrumb__toggle-btn--hidden {
40 | display: none;
41 | }
42 |
43 | .breadcrumb__text {
44 | white-space: nowrap;
45 | color: @primary_text_color;
46 | }
47 |
48 | .breadcrumb__text--button {
49 | cursor: pointer;
50 | color: @secondary_text_color;
51 |
52 | &:hover {
53 | text-decoration: underline;
54 | color: @main_color;
55 | }
56 | }
57 |
58 | .breadcrumb__text--ellipsis {
59 | .text-ellipsis();
60 | }
61 |
--------------------------------------------------------------------------------
/src/picker/css/material-icons-font.less:
--------------------------------------------------------------------------------
1 | // Copy from node_modules/material-icons-font/material-icons-font.css but remove
2 | // local sources since local font might be out of date.
3 | @font-face {
4 | font-family: "Material Icons";
5 | font-style: normal;
6 | font-weight: 400;
7 | src:
8 | url(../../../node_modules/material-icons-font/fonts/MaterialIcons-Regular.woff2) format('woff2'),
9 | url(../../../node_modules/material-icons-font/fonts/MaterialIcons-Regular.woff) format('woff');
10 | }
11 |
12 | .material-icons {
13 | display: inline-block;
14 | font-size: 24px; /* Preferred icon size */
15 | font-family: "Material Icons";
16 | font-weight: normal;
17 | font-style: normal;
18 | line-height: 1;
19 | text-transform: none;
20 | letter-spacing: normal;
21 | word-wrap: normal;
22 | white-space: nowrap;
23 | direction: ltr;
24 |
25 | /* Support for all WebKit browsers. */
26 | -webkit-font-smoothing: antialiased;
27 |
28 | /* Support for Safari and Chrome. */
29 | text-rendering: optimizeLegibility;
30 |
31 | /* Support for Firefox. */
32 | -moz-osx-font-smoothing: grayscale;
33 |
34 | /* Support for IE. */
35 | font-feature-settings: 'liga';
36 | }
37 |
--------------------------------------------------------------------------------
/test/dist-test-server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | /** An express server to serve project root to test dist build
3 | * bind to port 3000
4 | */
5 | const express = require('express');
6 | const path = require('path');
7 | const http = require('http');
8 | const https = require('https');
9 | const sslCert = require('../dev-server/ssl-cert');
10 |
11 | const protocol = sslCert.certificate ? 'https' : 'http';
12 |
13 | const app = express();
14 |
15 | app.set('views', path.join(__dirname, './dist/'));
16 | app.set('view engine', 'ejs');
17 | app.set('port', 3000);
18 |
19 | app.get('/test/dist/', (req, res) => {
20 | res.render('index.ejs', {
21 | appId: process.env.KLOUDLESS_APP_ID || '',
22 | pickerUrl: `${protocol}://localhost:3000/dist/picker/index.html`,
23 | });
24 | });
25 | app.use(express.static(path.resolve(__dirname, '../')));
26 |
27 | let server;
28 |
29 | if (sslCert.certificate) {
30 | server = https.createServer(
31 | { key: sslCert.privateKey, cert: sslCert.certificate },
32 | app,
33 | );
34 | } else {
35 | server = http.createServer(app);
36 | }
37 |
38 | server.listen(app.get('port'), () => {
39 | console.log(
40 | `Dist-test server running on ${protocol}://localhost:3000/test/dist/`,
41 | );
42 | });
43 |
--------------------------------------------------------------------------------
/storybook-test/jest.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable max-len */
2 | const path = require('path');
3 |
4 | // For a detailed explanation regarding each configuration property, visit:
5 | // https://jestjs.io/docs/en/configuration.html
6 | module.exports = {
7 | // The root directory that Jest should scan for tests and modules within
8 | rootDir: path.resolve(__dirname),
9 |
10 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
11 | setupFilesAfterEnv: ['/tests/setupTests.js'],
12 |
13 | // The regexp pattern or array of patterns that Jest uses to detect test files
14 | testRegex: './*\\.test\\.js$',
15 |
16 | // A map from regular expressions to paths to transformers
17 | transform: {
18 | '^.+\\.[t|j]sx?$': 'babel-jest',
19 | },
20 |
21 | // A path to a module which exports an async function that is triggered once before all test suites
22 | globalSetup: '/tests/globalSetup.js',
23 |
24 | // A path to a module which exports an async function that is triggered once after all test suites
25 | globalTeardown: '/tests/globalTeardown.js',
26 |
27 | // The test environment that will be used for testing
28 | testEnvironment: '/tests/testEnvironment.js',
29 | };
30 |
--------------------------------------------------------------------------------
/src/loader/js/vue/Dropzone.js:
--------------------------------------------------------------------------------
1 | import filePicker from '../interface';
2 |
3 | const Dropzone = {
4 | name: 'dropzone',
5 | props: {
6 | options: {
7 | type: Object,
8 | required: true,
9 | },
10 | },
11 | data() {
12 | return {
13 | id: `dz-${Math.floor(Math.random() * (10 ** 12))}`,
14 | dropzone: null,
15 | };
16 | },
17 | methods: {
18 | initDropzone() {
19 | // deep clone options
20 | const options = JSON.parse(JSON.stringify(this.options));
21 | options.elementId = this.id;
22 | this.dropzone = filePicker.dropzone(options);
23 | this.dropzone.on('raw', (...args) => {
24 | if (args[0] === 'dropzoneClicked') {
25 | args[0] = 'click';
26 | }
27 | this.$emit(...args);
28 | });
29 | },
30 | },
31 | watch: {
32 | options: {
33 | handler() {
34 | this.dropzone.destroy();
35 | this.initDropzone();
36 | },
37 | deep: true,
38 | },
39 | },
40 | template: `
41 |
44 |
`,
45 | mounted() {
46 | this.initDropzone();
47 | },
48 | destroyed() {
49 | this.dropzone.destroy();
50 | },
51 | };
52 |
53 | export default Dropzone;
54 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint',
7 | ecmaFeatures: {
8 | jsx: true,
9 | },
10 | sourceType: 'module',
11 | },
12 | env: {
13 | browser: true,
14 | commonjs: true,
15 | es6: true,
16 | node: true,
17 | },
18 | globals: {
19 | $: 'readonly',
20 | },
21 | extends: [
22 | 'airbnb-base',
23 | 'plugin:react/recommended',
24 | ],
25 | plugins: [
26 | 'react',
27 | ],
28 | // add your custom rules here
29 | rules: {
30 | // allow async-await
31 | 'generator-star-spacing': 'off',
32 | // allow debugger during development
33 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
34 | // allow unresolved module and extensions, because webpack will handle
35 | // this part
36 | 'import/no-unresolved': 'off',
37 | 'import/extensions': 'off',
38 | 'import/no-extraneous-dependencies': 'off',
39 | // allow param reassign or the param's properties
40 | 'no-param-reassign': ['error', { props: false }],
41 | 'max-len': ['error', { code: 80 }],
42 | 'operator-linebreak': 'off',
43 | // allow prefix private functions with underscore
44 | 'no-underscore-dangle': 'off',
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/src/picker/js/dropdown.js:
--------------------------------------------------------------------------------
1 | function handleClickAway(e) {
2 | const toggleButtons = document.querySelectorAll('[data-dropdown-id]');
3 | toggleButtons.forEach((toggleButton) => {
4 | if (toggleButton.contains(e.target)) {
5 | return;
6 | }
7 | const dropdownId = toggleButton.getAttribute('data-dropdown-id');
8 | const dropdown = document.getElementById(dropdownId);
9 | if (dropdown) {
10 | dropdown.classList.remove('dropdown--toggled');
11 | }
12 | });
13 | }
14 |
15 | function toggleDropdown(e) {
16 | const { currentTarget, target } = e;
17 | const dropdownId = currentTarget.getAttribute('data-dropdown-id');
18 | const dropdown = document.getElementById(dropdownId);
19 | // exclude dropdown itself
20 | if (!dropdown || dropdown.contains(target)) {
21 | return;
22 | }
23 | dropdown.classList.toggle('dropdown--toggled');
24 | }
25 |
26 | export default function setupDropdown() {
27 | document.body.removeEventListener('click', handleClickAway);
28 | document.body.addEventListener('click', handleClickAway);
29 |
30 | const toggleButtons = document.querySelectorAll('[data-dropdown-id]');
31 | toggleButtons.forEach((toggleButton) => {
32 | toggleButton.removeEventListener('click', toggleDropdown);
33 | toggleButton.addEventListener('click', toggleDropdown);
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "zh": {
3 | "global": {
4 | "select": "选择",
5 | "cancel": "取消",
6 | "save": "保存",
7 | "upload": "上传"
8 | },
9 | "files": {
10 | "name": "名称",
11 | "size": "尺寸",
12 | "updated": "更新",
13 | "noFilesFound": "找不到文件",
14 | "untitledFolder": "无标题文件夹",
15 | "search": "搜索",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "如果您删除此帐户,则无法再浏览该帐户。 您确定吗?(此动作并不会将您登出该帐户)",
25 | "connectedAccounts": "关联帐户",
26 | "logout": "登出",
27 | "manage": "在此管理您的云存储帐户。",
28 | "chooseAccount": "欢迎! 请选择要连接的服务",
29 | "connectMore": "连接更多!",
30 | "upload": "连接更多!",
31 | "uploadFromComputer": "从您的计算机上传"
32 | },
33 | "selector": {
34 | "myComputer": "我的电脑",
35 | "accounts": "帐号"
36 | },
37 | "dropzone": {
38 | "message": "在此处拖放文件,或单击以打开文件资源管理器"
39 | },
40 | "computer": {
41 | "noSupport": "您的浏览器没有Flash,Silverlight或HTML5支持。"
42 | },
43 | "addConfirm": {
44 | "confirm": "确认帐户连接。",
45 | "clickBelow": "点击下方即可关联您的{serviceName}帐户",
46 | "connectAccount": "连接{serviceName}帐户"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/zh-TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "zh-TW": {
3 | "global": {
4 | "select": "選擇",
5 | "cancel": "取消",
6 | "save": "保存",
7 | "upload": "上傳"
8 | },
9 | "files": {
10 | "name": "名稱",
11 | "size": "尺寸",
12 | "updated": "更新",
13 | "noFilesFound": "找不到文件",
14 | "untitledFolder": "無標題文件夾",
15 | "search": "搜索",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "如果您刪除此帳戶,則無法再瀏覽該帳戶。 您確定嗎?(此動作並不會將您登出該帳戶)",
25 | "connectedAccounts": "關聯帳戶",
26 | "logout": "登出",
27 | "manage": "在此管理您的雲存儲帳戶。",
28 | "chooseAccount": "歡迎! 請選擇要連接的服務",
29 | "connectMore": "連接更多!",
30 | "upload": "上傳",
31 | "uploadFromComputer": "從您的計算機上傳"
32 | },
33 | "selector": {
34 | "myComputer": "我的電腦",
35 | "accounts": "帳號"
36 | },
37 | "dropzone": {
38 | "message": "在此處拖放文件,或單擊以打開文件資源管理器"
39 | },
40 | "computer": {
41 | "noSupport": "您的瀏覽器沒有Flash,Silverlight或HTML5支持。"
42 | },
43 | "addConfirm": {
44 | "confirm": "確認帳戶連接。",
45 | "clickBelow": "點擊下方即可關聯您的{serviceName}帳戶",
46 | "connectAccount": "連接{serviceName}帳戶"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "zh": {
3 | "global": {
4 | "select": "选择",
5 | "cancel": "取消",
6 | "save": "保存",
7 | "upload": "上传"
8 | },
9 | "files": {
10 | "name": "名称",
11 | "size": "尺寸",
12 | "updated": "更新",
13 | "noFilesFound": "找不到文件",
14 | "untitledFolder": "无标题文件夹",
15 | "search": "搜索",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "如果您删除此帐户,则无法再浏览该帐户。 您确定吗?(此动作并不会将您登出该帐户)",
25 | "connectedAccounts": "关联帐户",
26 | "logout": "登出",
27 | "manage": "在此管理您的云存储帐户。",
28 | "chooseAccount": "欢迎! 请选择要连接的服务",
29 | "connectMore": "连接更多!",
30 | "upload": "连接更多!",
31 | "uploadFromComputer": "从您的计算机上传"
32 | },
33 | "selector": {
34 | "myComputer": "我的电脑",
35 | "accounts": "帐号"
36 | },
37 | "dropzone": {
38 | "message": "在此处拖放文件,或单击以打开文件资源管理器"
39 | },
40 | "computer": {
41 | "noSupport": "您的浏览器没有Flash,Silverlight或HTML5支持。"
42 | },
43 | "addConfirm": {
44 | "confirm": "确认帐户连接。",
45 | "clickBelow": "点击下方即可关联您的{serviceName}帐户",
46 | "connectAccount": "连接{serviceName}帐户"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/templates/files.pug:
--------------------------------------------------------------------------------
1 | .container(data-bind=`css: { 'container--is-loading': (
2 | loading() && !loadingNextPage()) || processingConfirm() }`)
3 | .container__header
4 | .flex-row.align-items-center
5 | .flex-no-shrink.mr(data-bind="css: {'sm-hidden': $root.flavor() != 'chooser'}")
6 | .icon.icon--large(data-bind=`
7 | click: files.toggleSearchView.bind($data, true),
8 | css: { 'icon--disabled': loading, 'invisible': $root.flavor() != 'chooser'}`)
9 | .icon__search
10 | .flex-grow
11 | include selector
12 | .flex-no-shrink.ml(data-bind="css: {'sm-hidden': attachMode()}")
13 | .icon.icon--large(data-bind=`
14 | click: cancel,
15 | css: { 'invisible': attachMode() }`)
16 | .icon__close(data-bind='css: $root.e2eSelectors.J_CLOSE_BTN')
17 | .container__body
18 | .flex-col.flex-grow.h-100
19 | .flex-no-shrink.w-100
20 | .flex-row.mb
21 | .flex-grow.mr
22 | include breadcrumb
23 | .flex-no-shrink
24 | .icon.icon--button.icon--large(data-bind='click: files.refresh')
25 | .icon__refresh
26 | // ko if: files.allow_newdir
27 | .flex-no-shrink.ml
28 | .icon.icon--button.icon--large(data-bind='click: files.newdir')
29 | .icon__new-folder
30 | // /ko
31 | include filetable
32 |
33 | include footer
34 |
--------------------------------------------------------------------------------
/src/picker/css/buttons.less:
--------------------------------------------------------------------------------
1 | .btn {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | padding: 0 15px;
6 | width: 160px;
7 | height: 40px;
8 | text-align: center;
9 | border: 0;
10 | border-radius: @border_radius;
11 | outline: none;
12 | cursor: pointer;
13 |
14 | &:disabled {
15 | cursor: unset;
16 | }
17 | }
18 |
19 | .btn--primary {
20 | color: @primary_btn_text_color;
21 | background-color: @primary_btn_color;
22 | box-shadow: 0 1px 8px 1px @primary_btn_shadow_color;
23 |
24 | &:hover {
25 | background-color: @primary_btn_hover_color;
26 | }
27 |
28 | &:disabled {
29 | cursor: unset;
30 | color: @primary_btn_disable_text_color;
31 | background-color: @primary_btn_disable_color;
32 | }
33 | }
34 |
35 | .btn--secondary {
36 | color: @secondary_btn_text_color;
37 | background-color: @secondary_btn_color;
38 | box-shadow: 0 1px 8px 1px @secondary_btn_shadow_color;
39 |
40 | &:hover {
41 | background-color: @secondary_btn_hover_color;
42 | }
43 |
44 | &:disabled {
45 | cursor: unset;
46 | color: @secondary_btn_disable_text_color;
47 | background-color: @secondary_btn_disable_color;
48 | }
49 | }
50 |
51 | .link-btn {
52 | padding: 5px 0;
53 | font-size: @font_size_lg;
54 | text-align: center;
55 | color: @link_btn_text_color;
56 | cursor: pointer;
57 |
58 | &:hover {
59 | color: @link_btn_text_hover_color;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/picker/css/container.less:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
8 | .container__header {
9 | flex: 0 0 auto;
10 | padding: 12px;
11 | width: 100%;
12 | font-size: @font_size_lg;
13 | text-align: center;
14 | }
15 |
16 | .container__body {
17 | flex: 1 1 auto;
18 | overflow: hidden;
19 | padding: 12px 12px 0 12px;
20 | width: 100%;
21 | height: 100%;
22 | min-height: 0;
23 | }
24 |
25 | .container__body--scroll {
26 | overflow-y: auto;
27 | }
28 |
29 | .container__loading-wrapper {
30 | position: relative;
31 | width: 100%;
32 | height: 100%;
33 | }
34 |
35 | .container__loading {
36 | position: absolute;
37 | display: none;
38 | width: 100%;
39 | height: 100%;
40 | background-color: @loading_overlay_color;
41 | z-index: 1000;
42 | }
43 |
44 | .container--is-loading .container__loading {
45 | display: flex;
46 | align-items: center;
47 | justify-content: center;
48 | }
49 |
50 | .container__loading-icon {
51 | width: 21px;
52 | height: 21px;
53 | background: url(@icon_loading) no-repeat center center;
54 | background-size: contain;
55 | }
56 |
57 | .container__footer {
58 | flex: 0 0 auto;
59 | padding: 12px;
60 | width: 100%;
61 | font-size: @font_size_sm;
62 | text-align: center;
63 | color: @secondary_text_color;
64 | }
65 |
66 | .container__footer-link {
67 | text-decoration: none;
68 | color: @secondary_text_color;
69 | }
70 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "ko": {
3 | "global": {
4 | "select": "고르다",
5 | "cancel": "취소",
6 | "save": "구하다",
7 | "upload": "업로드"
8 | },
9 | "files": {
10 | "name": "이름",
11 | "size": "크기",
12 | "updated": "업데이트 됨",
13 | "noFilesFound": "파일을 찾을 수 없음",
14 | "untitledFolder": "제목없는 폴더",
15 | "search": "수색",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "이 계정을 볼 수 없습니다. 삭제 하시겠습니까? (이로 인해이 브라우저의 계정에서 로그 아웃되지 않는 점에 유의하십시오)",
25 | "connectedAccounts": "연결된 계정",
26 | "logout": "로그 아웃",
27 | "manage": "여기에 클라우드 스토리지 계정을 관리하십시오.",
28 | "chooseAccount": "환영! 연결할 서비스를 선택하십시오.",
29 | "connectMore": "더 많은 것을 연결하십시오!",
30 | "upload": "업로드",
31 | "uploadFromComputer": "컴퓨터에서 업로드"
32 | },
33 | "selector": {
34 | "myComputer": "내 컴퓨터",
35 | "accounts": "계정"
36 | },
37 | "dropzone": {
38 | "message": "여기에 파일을 끌어다 놓거나 클릭하여 파일 탐색기를 엽니 다."
39 | },
40 | "computer": {
41 | "noSupport": "브라우저에 Flash, Silverlight 또는 HTML5 지원 기능이 없습니다."
42 | },
43 | "addConfirm": {
44 | "confirm": "계정 연결을 확인하십시오.",
45 | "clickBelow": "{serviceName} 계정을 연결하려면 아래를 클릭하십시오.",
46 | "connectAccount": "{serviceName} 계정 연결"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/css/global.less:
--------------------------------------------------------------------------------
1 | .font-face(
2 | @font_face_name,
3 | @font_face_path,
4 | @font_face_format
5 | );
6 |
7 | body,
8 | html,
9 | a,
10 | p,
11 | span,
12 | div,
13 | input,
14 | textarea,
15 | button,
16 | h1,
17 | h2,
18 | h3,
19 | h4,
20 | h5,
21 | h6 {
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: auto;
24 | }
25 |
26 | html {
27 | box-sizing: border-box;
28 | font-size: @font_size_lg;
29 | font-family: @font_family;
30 | font-weight: 500;
31 | font-style: normal;
32 | font-stretch: normal;
33 | line-height: normal;
34 | letter-spacing: normal;
35 | color: @primary_text_color;
36 | background-color: @container_color;
37 | }
38 |
39 | body {
40 | width: 100vw;
41 | max-width: 100vw;
42 | height: 100vh;
43 | max-height: 100vh;
44 |
45 | .iexd {
46 | position: absolute;
47 | display: none;
48 | overflow: hidden;
49 | }
50 |
51 | #kloudless-file-picker {
52 | overflow-x: hidden;
53 | width: 100%;
54 | height: 100%;
55 | }
56 | }
57 |
58 | *,
59 | *::before,
60 | *::after {
61 | box-sizing: inherit;
62 | }
63 |
64 | table {
65 | width: 100%;
66 | background-color: @section_primary_bg_color;
67 | border-spacing: 0;
68 | }
69 |
70 | tbody {
71 | width: 100%;
72 | }
73 |
74 | td,
75 | th {
76 | padding: 10px;
77 | border-bottom: 1px solid @section_line_color;
78 | text-align: left;
79 |
80 | &:first-child {
81 | padding-right: 0;
82 | padding-left: 0;
83 | width: 20px;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "ja": {
3 | "global": {
4 | "select": "選択する",
5 | "cancel": "キャンセル",
6 | "save": "保存する",
7 | "upload": "アップロードする"
8 | },
9 | "files": {
10 | "name": "名",
11 | "size": "サイズ",
12 | "updated": "更新しました",
13 | "noFilesFound": "ファイルが見つかりません",
14 | "untitledFolder": "無題のフォルダ",
15 | "search": "サーチ",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "このアカウントを閲覧することはできなくなります。削除してもよろしいですか? (これにより、このブラウザのアカウントからログアウトされないことに注意してください)",
25 | "connectedAccounts": "接続アカウント",
26 | "logout": "ログアウト",
27 | "manage": "ここでクラウドストレージアカウントを管理します。",
28 | "chooseAccount": "ようこそ! 接続するサービスを選択してください",
29 | "connectMore": "もっとつなげよう!",
30 | "upload": "アップロードする",
31 | "uploadFromComputer": "パソコンからアップロードする"
32 | },
33 | "selector": {
34 | "myComputer": "私のコンピューター",
35 | "accounts": "アカウント"
36 | },
37 | "dropzone": {
38 | "message": "ここにファイルをドラッグアンドドロップするか、クリックしてファイルエクスプローラを開きます。"
39 | },
40 | "computer": {
41 | "noSupport": "お使いのブラウザはFlash、Silverlight、またはHTML5をサポートしていません。"
42 | },
43 | "addConfirm": {
44 | "confirm": "アカウントの接続を確認してください。",
45 | "clickBelow": "下をクリックして{serviceName}アカウントに接続してください",
46 | "connectAccount": "接続{serviceName}アカウント"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/templates/addconfirm.pug:
--------------------------------------------------------------------------------
1 | .container
2 | .container__header
3 | .flex-row.align-items-center
4 | .flex-no-shrink.icon.icon--large(
5 | data-bind='click: $root.setLocation.bind($data, "#/")')
6 | .icon__back
7 | .flex-grow.text-center(data-bind='translate: "addConfirm/confirm"')
8 | .container__body
9 | .flex-col.align-items-center.h-100.w-100
10 | .box.flex-grow
11 | .flex-col.justify-content-center.h-100
12 | .box__section
13 | div
14 | div.flex-row.justify-content-center.mt.mb(data-bind='translate: {html: { message: "addConfirm/clickBelow", variables: { serviceName: addConfirm.serviceName }}}')
15 | div.flex-row.justify-content-center.mb
16 | .flex-no-shrink.icon.icon--xlarge
17 | img.icon__service(data-bind='attr: {src: addConfirm.serviceLogo}')
18 | .flex-no-shrink.icon.icon--xlarge
19 | img.icon__service(data-bind='attr: {src: $root.static("/launch_site/support-plus.png")}')
20 | .flex-no-shrink.icon.icon--xlarge
21 | .icon__brand
22 | div.flex-row.justify-content-center.mb
23 | button.btn.btn--secondary.mr(data-bind=`
24 | click: $root.setLocation.bind($data, "#/"),
25 | translate: "global/cancel"`)
26 | button#confirm-add-button.btn.btn--primary(
27 | data-bind='translate: {html: { message: "addConfirm/connectAccount"}}')
28 | include footer
29 |
--------------------------------------------------------------------------------
/src/picker/js/models/search.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import $ from 'jquery';
3 | import logger from 'loglevel';
4 | import config from '../config';
5 | import util from '../util';
6 |
7 | 'use strict';
8 |
9 | //Create a search object
10 | var Search = function (account, key, query, rootFolderId = 'root') {
11 | this.account = account;
12 | this.key = key;
13 | this.q = query;
14 | this.results = null;
15 | this.request = null;
16 | this.rootFolderId = rootFolderId;
17 | };
18 |
19 | Search.prototype.search = function (callback, errback) {
20 | var self = this;
21 |
22 | let searchUrl = `/search/?q=${self.q}`;
23 | if (self.rootFolderId !== 'root') {
24 | searchUrl += `&parents=${self.rootFolderId}`;
25 | }
26 |
27 | self.request = $.ajax({
28 | url: config.getAccountUrl('storage', searchUrl),
29 | type: 'GET',
30 | headers: {
31 | Authorization: self.key.scheme + ' ' + self.key.key
32 | },
33 | success: function (data) {
34 | self.results = data;
35 | logger.debug('[Account ' + self.account + '] Search results on ',
36 | self.q, ': ', self.results);
37 | self.results.objects = self.results.objects.map((obj)=>{
38 | obj.friendlySize = util.getFriendlySize(obj.size);
39 | return obj;
40 | });
41 | if (callback) callback();
42 | },
43 | error: function () {
44 | logger.error('[Account ' + self.account + '] Search request failed.');
45 | if (errback) errback();
46 | },
47 | datatype: 'json'
48 | })
49 | };
50 |
51 | export default Search;
52 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/he.json:
--------------------------------------------------------------------------------
1 | {
2 | "he": {
3 | "global": {
4 | "select": "בחר",
5 | "cancel": "בטל",
6 | "save": "להציל",
7 | "upload": "העלה"
8 | },
9 | "files": {
10 | "name": "שם",
11 | "size": "גודל",
12 | "updated": "עודכן",
13 | "noFilesFound": "לא נמצאו קבצים",
14 | "untitledFolder": "תיקיה ללא כותרת",
15 | "search": "לחפש",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "לא תוכל עוד לדפדף בחשבון זה. האם אתה בטוח שברצונך להסיר אותו? (שים לב שזה לא מתנתק מהחשבון בדפדפן זה)",
25 | "connectedAccounts": "חשבונות מחוברים",
26 | "logout": "להתנתק",
27 | "manage": "נהל את חשבונות האחסון שלך בענן כאן..",
28 | "chooseAccount": "ברוך הבא! בחר שירות לחיבור",
29 | "connectMore": "התחבר עוד!",
30 | "upload": "העלה",
31 | "uploadFromComputer": "העלה מהמחשב שלך"
32 | },
33 | "selector": {
34 | "myComputer": "המחשב שלי",
35 | "accounts": "חשבונות"
36 | },
37 | "dropzone": {
38 | "message": "גרור ושחרר קבצים כאן, או לחץ כדי לפתוח את סייר הקבצים"
39 | },
40 | "computer": {
41 | "noSupport": "לדפדפן שלך אין תמיכה ב- Flash, Silverlight או ב- HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "אשר את החיבור לחשבון.",
45 | "clickBelow": "לחץ למטה כדי לחבר את חשבון {serviceName} שלך",
46 | "connectAccount": "התחבר לחשבון {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | We appreciate any and all third-party contributions to help improve our SDKs
2 | and UI Tools. When submitting any changes, please feel free to include your
3 | name and company (if applicable) here for posterity.
4 |
5 |
6 | Contributor offers to license certain software (a "Contribution" or multiple
7 | “Contributions”) to Kloudless, and Kloudless agrees to accept said
8 | Contributions, under the terms of the MIT open source license.
9 |
10 | Contributor understands and agrees that Kloudless shall have the irrevocable
11 | and perpetual right to make and distribute copies of any Contribution, as well
12 | as to create and distribute collective works and derivative works of any
13 | Contribution, under the MIT License.
14 |
15 | # Contributors
16 |
17 | * Leo Zhang [@ilikebits](https://github.com/ilikebits)
18 | * Timothy Liu [@pseudonumos](https://github.com/pseudonumos)
19 | * Vinod Chandru [@vinodc](https://github.com/vinodc)
20 | * Chris Kuehl [@chriskuehl](https://github.com/chriskuehl)
21 | * Edward Look [@edwlook](https://github.com/edwlook)
22 | * Steven Cheng [@chengsteven](https://github.com/chengsteven)
23 | * Alice Cai [@ahcai](https://github.com/ahcai)
24 | * Joeson Chiang [@joesonchiang](https://github.com/joesonchiang)
25 | * Jackson Broussard [@jbrsrd](https://github.com/jbrsrd)
26 | * Katie Low [@ktmellow](https://github.com/ktmellow)
27 | * Artem Pisarev [@artemas](https://github.com/artemas)
28 | * Phil Maclachlan [@flikstrr](https://github.com/flikstrr)
29 | * Anovysh Itechart [@anovysh-itechart](https://github.com/anovysh-itechart)
30 | * Tim Sewell [@timssewell](https://github.com/timssewell)
31 |
--------------------------------------------------------------------------------
/config/build.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | const argv = process.argv.slice(2);
5 |
6 | if (argv.length === 1) {
7 | // eslint-disable-next-line import/no-dynamic-require, global-require
8 | const config = require(path.resolve(__dirname, '../', argv[0]));
9 |
10 | webpack(config, (err, stats) => {
11 | if (err) {
12 | // eslint-disable-next-line no-console
13 | console.error(err.stack || err);
14 | if (err.details) {
15 | // eslint-disable-next-line no-console
16 | console.error(err.details);
17 | }
18 | // Do not print build msg if there is any error.
19 | return;
20 | }
21 |
22 | const info = stats.toJson();
23 |
24 | if (stats.hasErrors()) {
25 | // eslint-disable-next-line no-console
26 | console.error(info.errors);
27 | // Do not print build msg if there is any error.
28 | return;
29 | }
30 |
31 | if (stats.hasWarnings()) {
32 | // eslint-disable-next-line no-console
33 | console.warn(info.warnings);
34 | }
35 |
36 | process.stdout.write(`${
37 | stats.toString({
38 | colors: true,
39 | })}\n\n${
40 | process.env.BUILD_LICENSE === 'AGPL' ?
41 | 'This build is under AGPL license.\n' : (
42 | 'This build is under MIT license.\n' +
43 | 'Run `npm run build:agpl` to include support for ' +
44 | 'local uploads via the Computer option.\n')
45 | }`);
46 | });
47 | } else {
48 | // eslint-disable-next-line no-console
49 | console.error('Please provide the config filename as the first argument.');
50 | }
51 |
--------------------------------------------------------------------------------
/storybook-test/tests/globalSetup.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line max-len
2 | // https://github.com/smooth-code/jest-puppeteer#create-your-own-globalsetup-and-globalteardown
3 | const fs = require('fs');
4 | const os = require('os');
5 | const path = require('path');
6 | const { setup: setupPuppeteer } = require('jest-environment-puppeteer');
7 | const { setup: setupDevServer } = require('jest-dev-server');
8 | const { BASE_URL, KLOUDLESS_ACCOUNT_TOKEN, TEST_DATA } = require('../config');
9 | const ApiHelper = require('./integration/core/ApiHelper');
10 | const { devServerPorts } = require('../../config/common');
11 |
12 | const apiHelper = new ApiHelper(BASE_URL, KLOUDLESS_ACCOUNT_TOKEN);
13 |
14 | // Global setup is executed once before all workers starts.
15 | // In addition, the workers are run in different processes. We have to write
16 | // test data to the file so workers can get data from file.
17 | async function setupTestData() {
18 | const response = await apiHelper.listFolderContent('root');
19 | const data = { FOLDER_CONTENT: response.data.objects };
20 | const filepath = path.resolve(os.homedir(), TEST_DATA);
21 | fs.writeFileSync(filepath, JSON.stringify(data), { encoding: 'utf-8' });
22 | }
23 |
24 | module.exports = async function globalSetup(globalConfig) {
25 | await setupTestData();
26 | await setupPuppeteer(globalConfig);
27 | if (process.env.CI) {
28 | await setupDevServer([
29 | {
30 | command: 'npm run storybook',
31 | port: 9001,
32 | launchTimeout: 60000,
33 | },
34 | {
35 | command: 'npm run dev:story --prefix=../',
36 | port: devServerPorts.picker,
37 | launchTimeout: 60000,
38 | },
39 | ]);
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/fi.json:
--------------------------------------------------------------------------------
1 | {
2 | "fi": {
3 | "global": {
4 | "select": "valita",
5 | "cancel": "Peruuttaa",
6 | "save": "Tallentaa",
7 | "upload": "upload"
8 | },
9 | "files": {
10 | "name": "Nimi",
11 | "size": "Koko",
12 | "updated": "Päivitetty",
13 | "noFilesFound": "Ei tiedostoja",
14 | "untitledFolder": "Nimetön kansio",
15 | "search": "Hae",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Et enää voi selata tämän tilin. Oletko varma, että haluat poistaa sen? (Huomaa, että tämä ei kirjata ulos tilin tässä selaimessa)",
25 | "connectedAccounts": "Yhdistetyt tilit",
26 | "logout": "kirjautua ulos",
27 | "manage": "Hallitse pilvivarastotilejäsi täällä.",
28 | "chooseAccount": "Tervetuloa! Valitse palvelu, johon haluat muodostaa yhteyden",
29 | "connectMore": "Liitä lisää!",
30 | "upload": "upload",
31 | "uploadFromComputer": "Lataa tietokoneesta"
32 | },
33 | "selector": {
34 | "myComputer": "Tietokoneeni",
35 | "accounts": "tilit"
36 | },
37 | "dropzone": {
38 | "message": "Vedä ja pudota tiedostot täällä tai avaa File Picker"
39 | },
40 | "computer": {
41 | "noSupport": "Selaimesi ei sisällä Flash-, Silverlight- tai HTML5-tukea."
42 | },
43 | "addConfirm": {
44 | "confirm": "Vahvista tilin yhteys.",
45 | "clickBelow": "Yhdistä {serviceName} -tili napsauttamalla alla",
46 | "connectAccount": "Yhdistä {serviceName} -tili"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/th.json:
--------------------------------------------------------------------------------
1 | {
2 | "th-TH": {
3 | "global": {
4 | "select": "เลือก",
5 | "cancel": "ยกเลิก",
6 | "save": "บันทึก",
7 | "upload": "อัปโหลด"
8 | },
9 | "files": {
10 | "name": "ชื่อ",
11 | "size": "ขนาด",
12 | "updated": "Updated",
13 | "noFilesFound": "ไม่พบไฟล์",
14 | "untitledFolder": "โฟลเดอร์ไม่มีชื่อ",
15 | "search": "ค้นหา",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "คุณจะไม่สามารถเรียกดูบัญชีนี้ คุณแน่ใจหรือคุณต้องการที่จะลบมันได้หรือไม่ (หมายเหตุว่านี้ไม่ได้เข้าสู่ระบบคุณออกจากบัญชีในเบราว์เซอร์นี้)",
25 | "connectedAccounts": "บัญชีที่เชื่อมต่อ",
26 | "logout": "ออกจากระบบ",
27 | "manage": "จัดการบัญชีที่เก็บข้อมูลบนคลาวด์ของคุณที่นี่",
28 | "chooseAccount": "ยินดีต้อนรับ! โปรดเลือกบริการที่จะเชื่อมต่อ",
29 | "connectMore": "เชื่อมต่อมากขึ้น!",
30 | "upload": "อัปโหลด",
31 | "uploadFromComputer": "อัปโหลดจากคอมพิวเตอร์ของคุณ"
32 | },
33 | "selector": {
34 | "myComputer": "คอมพิวเตอร์ของฉัน",
35 | "accounts": "บัญชี"
36 | },
37 | "dropzone": {
38 | "message": "ลากและวางไฟล์ที่นี่หรือคลิกเพื่อเปิด File Picker"
39 | },
40 | "computer": {
41 | "noSupport": "เบราว์เซอร์ของคุณไม่มีการสนับสนุน Flash, Silverlight หรือ HTML5"
42 | },
43 | "addConfirm": {
44 | "confirm": "ยืนยันการเชื่อมต่อบัญชี",
45 | "clickBelow": "คลิกด้านล่างเพื่อเชื่อมต่อบัญชี {serviceName} ของคุณ",
46 | "connectAccount": "เชื่อมต่อบัญชี {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/et.json:
--------------------------------------------------------------------------------
1 | {
2 | "et": {
3 | "global": {
4 | "select": "Valige",
5 | "cancel": "Tühista",
6 | "save": "Salvesta",
7 | "upload": "Laadi üles"
8 | },
9 | "files": {
10 | "name": "Nimi",
11 | "size": "Suurus",
12 | "updated": "Uuendatud",
13 | "noFilesFound": "Faile ei leitud",
14 | "untitledFolder": "Pealkirjata kaust",
15 | "search": "Otsing",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Te ei saa seda kontot enam sirvida. Kas olete kindel, et soovite selle eemaldada? (Pange tähele, et see ei logi teid selle brauseri kontolt välja)",
25 | "connectedAccounts": "Ühendatud kontod",
26 | "logout": "Logi välja",
27 | "manage": "Hallake oma pilvesalvestuskontot siin.",
28 | "chooseAccount": "Tere tulemast! Palun valige ühendatav teenus",
29 | "connectMore": "Ühendage rohkem!",
30 | "upload": "Laadi üles",
31 | "uploadFromComputer": "Laadige oma arvutisse üles"
32 | },
33 | "selector": {
34 | "myComputer": "Minu arvuti",
35 | "accounts": "Kontod"
36 | },
37 | "dropzone": {
38 | "message": "Lohistage failid siia ja klõpsake File Picker avamiseks"
39 | },
40 | "computer": {
41 | "noSupport": "Teie brauseril puudub Flash, Silverlight või HTML5 tugi."
42 | },
43 | "addConfirm": {
44 | "confirm": "Kinnitage konto ühendus.",
45 | "clickBelow": "Klõpsake allpool, et ühendada oma {serviceName} konto",
46 | "connectAccount": "Ühenda {serviceName} konto"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/cs.json:
--------------------------------------------------------------------------------
1 | {
2 | "cs": {
3 | "global": {
4 | "select": "Vybrat",
5 | "cancel": "zrušení",
6 | "save": "Uložit",
7 | "upload": "nahrát"
8 | },
9 | "files": {
10 | "name": "název",
11 | "size": "Velikost",
12 | "updated": "Aktualizováno",
13 | "noFilesFound": "Nebyly nalezeny žádné soubory",
14 | "untitledFolder": "Složka bez názvu",
15 | "search": "Vyhledávání",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Již nebude moci procházet tento účet. Jsou si jisti, že chcete odstranit? (Všimněte si, že to není odhlásit z účtu v tomto prohlížeči)",
25 | "connectedAccounts": "Připojené účty",
26 | "logout": "odhlásit se",
27 | "manage": "Zde můžete spravovat účty cloudového úložiště.",
28 | "chooseAccount": "Vítejte! Vyberte službu, ke které se chcete připojit",
29 | "connectMore": "Připojte více!",
30 | "upload": "nahrát",
31 | "uploadFromComputer": "Nahrát z počítače"
32 | },
33 | "selector": {
34 | "myComputer": "Můj počítač",
35 | "accounts": "Účty"
36 | },
37 | "dropzone": {
38 | "message": "Přetáhněte soubory sem nebo kliknutím otevřete Průzkumník souborů"
39 | },
40 | "computer": {
41 | "noSupport": "Váš prohlížeč nemá podporu Flash, Silverlight nebo HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Potvrďte připojení účtu.",
45 | "clickBelow": "Klikněte níže a připojte svůj účet {serviceName}",
46 | "connectAccount": "Připojit účet {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/sv.json:
--------------------------------------------------------------------------------
1 | {
2 | "sv": {
3 | "global": {
4 | "select": "Välj",
5 | "cancel": "Annullera",
6 | "save": "Spara",
7 | "upload": "Ladda upp"
8 | },
9 | "files": {
10 | "name": "namn",
11 | "size": "Storlek",
12 | "updated": "Uppdaterad",
13 | "noFilesFound": "Inga filer funna",
14 | "untitledFolder": "Untitled Folder",
15 | "search": "Sök",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Du kommer inte längre att kunna surfa på kontot. Är du säker på att du vill ta bort den? (Observera att detta inte loggar du ut från kontot på denna webbläsare)",
25 | "connectedAccounts": "Anslutna konton",
26 | "logout": "logga ut",
27 | "manage": "Hantera dina Cloud Storage-konton här.",
28 | "chooseAccount": "Välkommen! Välj en tjänst för att ansluta",
29 | "connectMore": "Anslut mer!",
30 | "upload": "Ladda upp",
31 | "uploadFromComputer": "Ladda upp från din dator"
32 | },
33 | "selector": {
34 | "myComputer": "Min dator",
35 | "accounts": "konton"
36 | },
37 | "dropzone": {
38 | "message": "Dra och släpp filer här, eller klicka för att öppna File Picker"
39 | },
40 | "computer": {
41 | "noSupport": "Din webbläsare har inte stöd för Flash, Silverlight eller HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Bekräfta kontoanslutning.",
45 | "clickBelow": "Klicka nedan för att ansluta ditt {serviceName} -konto",
46 | "connectAccount": "Anslut {serviceName} konto"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/da.json:
--------------------------------------------------------------------------------
1 | {
2 | "da": {
3 | "global": {
4 | "select": "Vælg",
5 | "cancel": "Afbestille",
6 | "save": "Gemme",
7 | "upload": "Upload"
8 | },
9 | "files": {
10 | "name": "Navn",
11 | "size": "Størrelse",
12 | "updated": "Opdateret",
13 | "noFilesFound": "Ingen filer fundet",
14 | "untitledFolder": "Untitled Folder",
15 | "search": "Søg",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Du vil ikke længere være i stand til at gennemse denne konto. Er du sikker på du ønsker at fjerne det? (Bemærk at dette ikke logge dig ud af kontoen på denne browser)",
25 | "connectedAccounts": "Tilknyttede konti",
26 | "logout": "Log ud",
27 | "manage": "Administrer dine Cloud Storage-konti her.",
28 | "chooseAccount": "Velkommen! Vælg venligst en tjeneste for at oprette forbindelse",
29 | "connectMore": "Tilslut mere!",
30 | "upload": "Upload",
31 | "uploadFromComputer": "Upload fra din computer"
32 | },
33 | "selector": {
34 | "myComputer": "Min computer",
35 | "accounts": "Konti"
36 | },
37 | "dropzone": {
38 | "message": "Træk og slip filer her, eller klik for at åbne Filoversigten"
39 | },
40 | "computer": {
41 | "noSupport": "Din browser har ikke Flash, Silverlight eller HTML5 support."
42 | },
43 | "addConfirm": {
44 | "confirm": "Bekræft kontoforbindelse.",
45 | "clickBelow": "Klik nedenfor for at forbinde din {serviceName} konto",
46 | "connectAccount": "Tilslut {serviceName} Konto"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/tr.json:
--------------------------------------------------------------------------------
1 | {
2 | "tr": {
3 | "global": {
4 | "select": "seçmek",
5 | "cancel": "İptal etmek",
6 | "save": "Kayıt etmek",
7 | "upload": "Yükleme"
8 | },
9 | "files": {
10 | "name": "isim",
11 | "size": "Boyut",
12 | "updated": "Güncellenmiş",
13 | "noFilesFound": "Dosya Bulunamadı",
14 | "untitledFolder": "Adsız Klasör",
15 | "search": "Arama",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Artık bu hesaba göz atamayacaksınız. Kaldırmak istediğinizden emin misiniz? (Bunun sizi bu tarayıcıdaki hesaptan çıkmadığını unutmayın)",
25 | "connectedAccounts": "Bağlı hesaplar",
26 | "logout": "çıkış Yap",
27 | "manage": "Bulut depolama hesaplarınızı burada yönetin.",
28 | "chooseAccount": "Hoşgeldiniz! Lütfen bağlanmak için bir servis seçin",
29 | "connectMore": "Daha fazlasını bağla!",
30 | "upload": "Yükleme",
31 | "uploadFromComputer": "Bilgisayarınızdan yükleyin"
32 | },
33 | "selector": {
34 | "myComputer": "Benim bilgisayarım",
35 | "accounts": "Hesaplar"
36 | },
37 | "dropzone": {
38 | "message": "Dosyaları buraya sürükleyip bırakın veya Dosya Gezgini'ni açmak için tıklayın."
39 | },
40 | "computer": {
41 | "noSupport": "Tarayıcınızda Flash, Silverlight veya HTML5 desteği bulunmuyor."
42 | },
43 | "addConfirm": {
44 | "confirm": "Hesap bağlantısını onayla.",
45 | "clickBelow": "{ServiceName} hesabınızı bağlamak için aşağıyı tıklayın",
46 | "connectAccount": "Connect {serviceName} Hesabı"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/pl.json:
--------------------------------------------------------------------------------
1 | {
2 | "pl": {
3 | "global": {
4 | "select": "Wybierz",
5 | "cancel": "Anuluj",
6 | "save": "Zapisać",
7 | "upload": "Przekazać plik"
8 | },
9 | "files": {
10 | "name": "Imię",
11 | "size": "Rozmiar",
12 | "updated": "Zaktualizowano",
13 | "noFilesFound": "Nie znaleziono plików",
14 | "untitledFolder": "Folder bez tytułu",
15 | "search": "Szukaj",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Państwo nie będzie już w stanie przeglądać tego konta. Czy na pewno chcesz go usunąć? (Należy pamiętać, że to nie wylogowanie z konta w tej przeglądarce)",
25 | "connectedAccounts": "Połączone konta",
26 | "logout": "Wyloguj",
27 | "manage": "Zarządzaj swoimi kontami przechowywania w chmurze tutaj.",
28 | "chooseAccount": "Witamy! Wybierz usługę do połączenia",
29 | "connectMore": "Połącz więcej!",
30 | "upload": "Przekazać plik",
31 | "uploadFromComputer": "Prześlij z komputera"
32 | },
33 | "selector": {
34 | "myComputer": "Mój komputer",
35 | "accounts": "Konta"
36 | },
37 | "dropzone": {
38 | "message": "Przeciągnij i upuść pliki tutaj lub kliknij, aby otworzyć Eksplorator plików"
39 | },
40 | "computer": {
41 | "noSupport": "Twoja przeglądarka nie obsługuje Flash, Silverlight ani HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Potwierdź połączenie z kontem.",
45 | "clickBelow": "Kliknij poniżej, aby połączyć swoje konto {serviceName}",
46 | "connectAccount": "Połącz konto {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "pt": {
3 | "global": {
4 | "select": "Selecione",
5 | "cancel": "Cancelar",
6 | "save": "Salve",
7 | "upload": "Envio"
8 | },
9 | "files": {
10 | "name": "Nome",
11 | "size": "Tamanho",
12 | "updated": "Atualizada",
13 | "noFilesFound": "Nenhum arquivo encontrado",
14 | "untitledFolder": "Pasta sem título",
15 | "search": "Procurar",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Você não será mais capaz de navegar esta conta. Tem certeza de que gostaria de removê-lo? (Note que isso não desconectá-lo da conta neste navegador)",
25 | "connectedAccounts": "Contas conectadas",
26 | "logout": "sair",
27 | "manage": "Gerencie suas contas de armazenamento em nuvem aqui.",
28 | "chooseAccount": "Bem vinda! Por favor, escolha um serviço para conectar",
29 | "connectMore": "Ligue mais!",
30 | "upload": "Envio",
31 | "uploadFromComputer": "Upload do seu computador"
32 | },
33 | "selector": {
34 | "myComputer": "Meu computador",
35 | "accounts": "Contas"
36 | },
37 | "dropzone": {
38 | "message": "Arraste e solte arquivos aqui ou clique para abrir o Gerenciador de arquivos"
39 | },
40 | "computer": {
41 | "noSupport": "Seu navegador não tem suporte para Flash, Silverlight ou HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Confirme a conexão da conta.",
45 | "clickBelow": "Clique abaixo para conectar sua conta do {serviceName}",
46 | "connectAccount": "Conecte a conta {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/id.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": {
3 | "global": {
4 | "select": "Memilih",
5 | "cancel": "Membatalkan",
6 | "save": "Menyimpan",
7 | "upload": "Unggah"
8 | },
9 | "files": {
10 | "name": "Nama",
11 | "size": "Ukuran",
12 | "updated": "Diperbarui",
13 | "noFilesFound": "Tidak Ada File",
14 | "untitledFolder": "Folder Tanpa Judul",
15 | "search": "Pencarian",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Anda tidak lagi dapat menelusuri akun ini. Apakah Anda yakin ingin menghapusnya? (Perhatikan bahwa ini tidak membuat Anda keluar dari akun di browser ini)",
25 | "connectedAccounts": "Akun yang terhubung",
26 | "logout": "keluar",
27 | "manage": "Kelola akun penyimpanan cloud Anda di sini.",
28 | "chooseAccount": "Selamat datang! Silakan pilih layanan yang akan dihubungkan",
29 | "connectMore": "Hubungkan lebih banyak!",
30 | "upload": "Unggah",
31 | "uploadFromComputer": "Unggah dari komputer Anda"
32 | },
33 | "selector": {
34 | "myComputer": "Komputer saya",
35 | "accounts": "Akun"
36 | },
37 | "dropzone": {
38 | "message": "Seret dan taruh file di sini, atau klik untuk membuka File Picker"
39 | },
40 | "computer": {
41 | "noSupport": "Browser Anda tidak memiliki dukungan Flash, Silverlight atau HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Konfirmasikan koneksi akun.",
45 | "clickBelow": "Klik di bawah untuk menghubungkan akun {serviceName} Anda",
46 | "connectAccount": "Hubungkan {serviceName} Akun"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/nl.json:
--------------------------------------------------------------------------------
1 | {
2 | "nl": {
3 | "global": {
4 | "select": "kiezen",
5 | "cancel": "annuleren",
6 | "save": "Opslaan",
7 | "upload": "Uploaden"
8 | },
9 | "files": {
10 | "name": "Naam",
11 | "size": "Grootte",
12 | "updated": "bijgewerkt",
13 | "noFilesFound": "Geen bestanden gevonden",
14 | "untitledFolder": "Map zonder titel",
15 | "search": "Zoeken",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "U zult niet langer in staat zijn om deze account te bladeren. Bent u zeker dat u wilt om het te verwijderen? (Merk op dat dit niet je uitgelogd van het account op deze browser)",
25 | "connectedAccounts": "Verbonden accounts",
26 | "logout": "uitloggen",
27 | "manage": "Beheer hier uw cloudopslag-accounts.",
28 | "chooseAccount": "Welkom! Kies een service om verbinding te maken",
29 | "connectMore": "Verbind meer!",
30 | "upload": "Uploaden",
31 | "uploadFromComputer": "Upload vanaf uw computer"
32 | },
33 | "selector": {
34 | "myComputer": "Mijn computer",
35 | "accounts": "accounts"
36 | },
37 | "dropzone": {
38 | "message": "Versleep hier bestanden, of klik om de Verkenner te openen"
39 | },
40 | "computer": {
41 | "noSupport": "Uw browser ondersteunt geen Flash-, Silverlight- of HTML5-ondersteuning."
42 | },
43 | "addConfirm": {
44 | "confirm": "Bevestig de accountverbinding.",
45 | "clickBelow": "Klik hieronder om verbinding te maken met uw {serviceName} account",
46 | "connectAccount": "Verbind {serviceName} account"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/dev-server/picker-template-hot-loader.js:
--------------------------------------------------------------------------------
1 | /* global $, ko */
2 | /**
3 | * Hot reload file picker templates in dev-server.
4 | * Support hot reloading templates without reloading the file picker page.
5 | *
6 | * For now, the hot reload functionality is limited:
7 | * 1. We need to update index.pug once after page load to make hot
8 | * reloading start working. I.E. editing other pug files won't trigger
9 | * hot reload until index.pug is hot reloaded once.
10 | * One workaround is to add an empty div under #kloudless-file-explore, save,
11 | * then revert.
12 | *
13 | * 2. The hot reload function assumes a div with id="kloudless-file-picker"
14 | * exists in index.pug and is served as the main container for
15 | * file picker templates.
16 | */
17 |
18 | import getTemplateHtml from 'picker/templates/index.pug';
19 |
20 | function onHotAccept() {
21 | // getTemplateHtml has been reloaded when this callback is executed
22 |
23 | /* eslint-disable no-console */
24 | console.log('Hot reloading file picker templates...');
25 |
26 | // remove current ko binding and HTML
27 | let $picker = $('#kloudless-file-picker');
28 | ko.cleanNode($picker[0]);
29 | $('#kloudless-file-picker').remove();
30 |
31 | // file picker is exposed in window when in development mode
32 | const { picker } = window;
33 |
34 | // insert updated templates and rebind ko
35 | $('body').prepend(getTemplateHtml());
36 | // get the newly rendered template DOM reference
37 | $picker = $('#kloudless-file-picker');
38 | ko.applyBindings(picker.view_model, $picker[0]);
39 |
40 | // force reloading jquery plugins like dropdown and plupload
41 | picker.switchViewTo(picker.view_model.current());
42 |
43 | console.log('[Done] Hot reloaded file picker templates.');
44 | }
45 |
46 | module.hot.accept('picker/templates/index.pug', onHotAccept);
47 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/it.json:
--------------------------------------------------------------------------------
1 | {
2 | "it": {
3 | "global": {
4 | "select": "Selezionare",
5 | "cancel": "Annulla",
6 | "save": "Salvare",
7 | "upload": "Caricare"
8 | },
9 | "files": {
10 | "name": "Nome",
11 | "size": "Taglia",
12 | "updated": "aggiornato",
13 | "noFilesFound": "Nessun file trovato",
14 | "untitledFolder": "Cartella senza titolo",
15 | "search": "Ricerca",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Non sarà più in grado di navigare questo account. Sei sicuro di voler rimuovere esso? (Si noti che questo non ti logout dell'account su questo browser)",
25 | "connectedAccounts": "Account collegati",
26 | "logout": "disconnettersi",
27 | "manage": "Gestisci i tuoi account di archiviazione cloud qui.",
28 | "chooseAccount": "Benvenuto! Si prega di scegliere un servizio per la connessione",
29 | "connectMore": "Connetti di più!",
30 | "upload": "Caricare",
31 | "uploadFromComputer": "Carica dal tuo computer"
32 | },
33 | "selector": {
34 | "myComputer": "Il mio computer",
35 | "accounts": "conti"
36 | },
37 | "dropzone": {
38 | "message": "Trascina qui i file o fai clic per aprire Esplora file"
39 | },
40 | "computer": {
41 | "noSupport": "Il tuo browser non ha supporto Flash, Silverlight o HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Conferma la connessione dell'account.",
45 | "clickBelow": "Fai clic di seguito per connettere il tuo account {serviceName}",
46 | "connectAccount": "Connetti l'account {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/ro.json:
--------------------------------------------------------------------------------
1 | {
2 | "ro": {
3 | "global": {
4 | "select": "Selectați",
5 | "cancel": "Anulare",
6 | "save": "Salvați",
7 | "upload": "Încărcați"
8 | },
9 | "files": {
10 | "name": "Nume",
11 | "size": "mărimea",
12 | "updated": "La curent",
13 | "noFilesFound": "Niciun fișier găsit",
14 | "untitledFolder": "Folder fără titlu",
15 | "search": "Căutare",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Nu veți mai putea naviga în acest cont. Sunteți sigur că doriți să îl eliminați? (Rețineți că acest lucru nu vă deconectează din cont în acest browser)",
25 | "connectedAccounts": "Conturi conectate",
26 | "logout": "logout",
27 | "manage": "Gestionați-vă aici conturile de stocare în cloud.",
28 | "chooseAccount": "Bine ati venit! Alegeți un serviciu pentru conectare",
29 | "connectMore": "Conectează-te mai mult!",
30 | "upload": "Încărcați",
31 | "uploadFromComputer": "Încărcați de pe computer"
32 | },
33 | "selector": {
34 | "myComputer": "Calculatorul meu",
35 | "accounts": "Conturi"
36 | },
37 | "dropzone": {
38 | "message": "Glisați și fixați fișierele aici sau faceți clic pentru a deschide fișierul Picker"
39 | },
40 | "computer": {
41 | "noSupport": "Browserul dvs. nu are suport Flash, Silverlight sau HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Confirmați conexiunea la cont.",
45 | "clickBelow": "Faceți clic mai jos pentru a vă conecta contul {serviceName}",
46 | "connectAccount": "Conectați contul {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "es": {
3 | "global": {
4 | "select": "Seleccionar",
5 | "cancel": "Cancelar",
6 | "save": "Salvar",
7 | "upload": "Subir"
8 | },
9 | "files": {
10 | "name": "Nombre",
11 | "size": "tamaño",
12 | "updated": "Actualizado",
13 | "noFilesFound": "No se encontraron archivos",
14 | "untitledFolder": "Carpeta sin título",
15 | "search": "Buscar",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Ya no podrás navegar por esta cuenta. ¿Estás seguro de que deseas eliminarlo? (Tenga en cuenta que esto no cierra la sesión de la cuenta en este navegador)",
25 | "connectedAccounts": "Cuentas conectadas",
26 | "logout": "cerrar sesión",
27 | "manage": "Gérez vos comptes de stockage en nuage ici.",
28 | "chooseAccount": "¡Bienvenido! Por favor, elija un servicio para conectarse",
29 | "connectMore": "¡Conecta más!",
30 | "upload": "Subir",
31 | "uploadFromComputer": "Sube desde tu computadora"
32 | },
33 | "selector": {
34 | "myComputer": "Mi computadora",
35 | "accounts": "Cuentas"
36 | },
37 | "dropzone": {
38 | "message": "Arrastre y suelte los archivos aquí, o haga clic para abrir el Explorador de archivos"
39 | },
40 | "computer": {
41 | "noSupport": "Su navegador no tiene Flash, Silverlight o HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Confirmar la conexión de la cuenta.",
45 | "clickBelow": "Haga clic a continuación para conectar su cuenta {serviceName}",
46 | "connectAccount": "Conectar cuenta {serviceName}"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "ru": {
3 | "global": {
4 | "select": "Выбрать",
5 | "cancel": "отменить",
6 | "save": "Сохранить",
7 | "upload": "Загрузить"
8 | },
9 | "files": {
10 | "name": "название",
11 | "size": "Размер",
12 | "updated": "обновленный",
13 | "noFilesFound": "Файлы не найдены",
14 | "untitledFolder": "Папка без названия",
15 | "search": "Поиск",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Вы больше не сможете просматривать эту учетную запись. Вы уверены, что хотите удалить его? (Обратите внимание, что это не выходит из учетной записи в этом браузере)",
25 | "connectedAccounts": "Подключенные учетные записи",
26 | "logout": "выйти",
27 | "manage": "Управляйте своими учетными записями облачного хранилища здесь.",
28 | "chooseAccount": "Добро пожаловать! Пожалуйста, выберите услугу для подключения",
29 | "connectMore": "Подключи больше!",
30 | "upload": "Загрузить",
31 | "uploadFromComputer": "Загрузить с вашего компьютера"
32 | },
33 | "selector": {
34 | "myComputer": "Мой компьютер",
35 | "accounts": "Счета"
36 | },
37 | "dropzone": {
38 | "message": "Перетащите файлы сюда или нажмите, чтобы открыть проводник"
39 | },
40 | "computer": {
41 | "noSupport": "Ваш браузер не поддерживает Flash, Silverlight или HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Подтвердите подключение аккаунта.",
45 | "clickBelow": "Нажмите ниже, чтобы подключить свою учетную запись {serviceName}",
46 | "connectAccount": "Подключить {serviceName} аккаунт"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "fr": {
3 | "global": {
4 | "select": "Sélectionner",
5 | "cancel": "Annuler",
6 | "save": "sauvegarder",
7 | "upload": "Télécharger"
8 | },
9 | "files": {
10 | "name": "prénom",
11 | "size": "Taille",
12 | "updated": "Mis à jour",
13 | "noFilesFound": "Aucun fichier trouvé",
14 | "untitledFolder": "Dossier sans titre",
15 | "search": "Chercher",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Vous ne serez plus en mesure de parcourir ce compte. Etes-vous sûr que vous souhaitez supprimer? (Notez que cela ne vous déconnecte pas du compte sur ce navigateur)",
25 | "connectedAccounts": "Comptes connectés",
26 | "logout": "Connectez - Out",
27 | "manage": "Gérez vos comptes de stockage en nuage ici.",
28 | "chooseAccount": "Bienvenue! Veuillez choisir un service pour vous connecter",
29 | "connectMore": "Connectez plus!",
30 | "upload": "Télécharger",
31 | "uploadFromComputer": "Télécharger depuis votre ordinateur"
32 | },
33 | "selector": {
34 | "myComputer": "Mon ordinateur",
35 | "accounts": "Comptes"
36 | },
37 | "dropzone": {
38 | "message": "Glissez et déposez les fichiers ici, ou cliquez pour ouvrir l'explorateur de fichiers"
39 | },
40 | "computer": {
41 | "noSupport": "Votre navigateur ne prend pas en charge Flash, Silverlight ou HTML5."
42 | },
43 | "addConfirm": {
44 | "confirm": "Confirmer la connexion au compte.",
45 | "clickBelow": "Cliquez ci-dessous pour connecter votre compte {serviceName}",
46 | "connectAccount": "Connecter {serviceName} compte"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/dev-server/dev-server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | if (!process.env.KLOUDLESS_APP_ID) {
4 | console.log('Environment variable KLOUDLESS_APP_ID not specified.');
5 | process.exit(1);
6 | }
7 |
8 | const webpackDevMiddleware = require('webpack-dev-middleware');
9 | const webpackHotMiddleware = require('webpack-hot-middleware');
10 | const express = require('express');
11 | const morgan = require('morgan');
12 | const path = require('path');
13 | const http = require('http');
14 | const https = require('https');
15 | const webpack = require('webpack');
16 | const sslCert = require('./ssl-cert');
17 |
18 | const webpackConfigs = require('../config/webpack.dev.conf');
19 |
20 | const app = express();
21 |
22 | process.env.PICKER_URL = '/picker/index.html';
23 |
24 | app.set('port', process.env.PORT || 3000);
25 | app.use(morgan('dev'));
26 | app.use('/static', express.static(path.join(__dirname, './static')));
27 |
28 | app.use((req, res, next) => {
29 | res.set({
30 | 'Cache-Control': 'no-cache, no-store, must-revalidate',
31 | Pragma: 'no-cache',
32 | Expires: 0,
33 | });
34 | next();
35 | });
36 |
37 | const compiler = webpack(webpackConfigs);
38 | const middleware = webpackDevMiddleware(compiler, {
39 | logTime: true,
40 | stats: 'minimal',
41 | publicPath: webpackConfigs[0].output.publicPath,
42 | });
43 | app.use(middleware);
44 | app.use(webpackHotMiddleware(compiler, {
45 | log: false, path: '/__webpack_hmr', heartbeat: 10 * 1000,
46 | }));
47 |
48 | let server;
49 |
50 | if (sslCert.certificate) {
51 | server = https.createServer(
52 | { key: sslCert.privateKey, cert: sslCert.certificate },
53 | app,
54 | );
55 | } else {
56 | server = http.createServer(app);
57 | }
58 |
59 | server.listen(app.get('port'), () => {
60 | console.log('Dev server running on http://localhost:3000');
61 | console.log('Webpack bundles are compiling...');
62 | });
63 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const common = require('./config/common');
3 | const packages = require('./package.json');
4 |
5 |
6 | /**
7 | * Define build options environment variables here
8 | * format: [var name]: default value
9 | * check README for variable names and purpose
10 | */
11 | const buildEnvVarDefaults = {
12 | PICKER_URL:
13 | 'https://static-cdn.kloudless.com/p/platform/file-picker/v2/index.html',
14 | // old version that supports custom_css
15 | PICKER_URL_V1:
16 | 'https://static-cdn.kloudless.com/p/platform/explorer/explorer.html',
17 | BASE_URL: 'https://api.kloudless.com',
18 |
19 | // for development only
20 | KLOUDLESS_APP_ID: null,
21 | // 'MIT' or 'AGPL'.
22 | // The MIT build excludes the plupload module.
23 | BUILD_LICENSE: 'MIT',
24 | };
25 |
26 | const transformDefines = {
27 | VERSION: packages.version,
28 | };
29 |
30 | Object.keys(buildEnvVarDefaults).forEach((varName) => {
31 | transformDefines[varName] = (
32 | process.env[varName] || buildEnvVarDefaults[varName]);
33 | });
34 |
35 | module.exports = {
36 | presets: [
37 | ['@babel/preset-env', {
38 | useBuiltIns: 'usage',
39 | corejs: 3,
40 | }],
41 | // Used by storybook-react
42 | '@babel/preset-react',
43 | ],
44 | ignore: common.ignorePaths,
45 | plugins: [
46 | [
47 | 'module-resolver', {
48 | // avoid node_modules path being relative path
49 | root: common.resolvePaths.filter(p => p !== 'node_modules'),
50 | alias: {
51 | 'picker-config': path.resolve(__dirname, './src/picker/js/',
52 | (process.env.NODE_ENV === 'production'
53 | && !JSON.parse(process.env.DEBUG || false)) ?
54 | './config_prod.json' : './config.json'),
55 | },
56 | },
57 | ],
58 | [
59 | 'transform-define', transformDefines,
60 | ],
61 | ],
62 | };
63 |
--------------------------------------------------------------------------------
/src/picker/js/breadcrumb.js:
--------------------------------------------------------------------------------
1 | const ICON_HTML = '';
2 | const ROOT_DIR_HTML = `
3 | `;
6 | const TOGGLE_BTN_HTML = `
7 |
8 | ${ICON_HTML}
9 |
...
10 |
`;
11 |
12 | /**
13 | * @param {Object[]} breadcrumbs
14 | * @param {string} breadcrumbs[].path
15 | * @param {boolean} breadcrumbs[].visible
16 | * @param {number} maxWidth
17 | */
18 | function isBreadcrumbOverflow(breadcrumbs, maxWidth) {
19 | const hiddenContainer = $('.breadcrumb__hidden');
20 | const showToggleButton = breadcrumbs.includes(e => e.visible === false);
21 | const els = [ROOT_DIR_HTML];
22 | if (showToggleButton) {
23 | els.push(TOGGLE_BTN_HTML);
24 | }
25 | breadcrumbs.filter(e => e.visible).forEach((breadcrumb) => {
26 | els.push(
27 | ICON_HTML,
28 | `${breadcrumb.path}
`,
29 | );
30 | });
31 | const html = `${els.join('')}
`;
32 | hiddenContainer.html(html);
33 | const width = hiddenContainer.outerWidth();
34 | return width >= maxWidth;
35 | }
36 |
37 | /**
38 | * @param {Object[]} breadcrumbs
39 | * @param {string} breadcrumbs[].path
40 | * @param {boolean} breadcrumbs[].visible
41 | */
42 | function adjustBreadcrumbWidth(breadcrumbs) {
43 | if (breadcrumbs === null || breadcrumbs.length <= 1) {
44 | return breadcrumbs;
45 | }
46 | const maxWidth = $('.breadcrumb').outerWidth();
47 | const { length } = breadcrumbs;
48 | let index = breadcrumbs.findIndex(e => e.visible);
49 | while (index < length - 1 && isBreadcrumbOverflow(breadcrumbs, maxWidth)) {
50 | breadcrumbs[index].visible = false;
51 | index += 1;
52 | }
53 | return breadcrumbs;
54 | }
55 |
56 | export default adjustBreadcrumbWidth;
57 |
--------------------------------------------------------------------------------
/src/picker/localization/messages/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "de": {
3 | "global": {
4 | "select": "Wählen",
5 | "cancel": "Stornieren",
6 | "save": "sparen",
7 | "upload": "Hochladen"
8 | },
9 | "files": {
10 | "name": "Name",
11 | "size": "Größe",
12 | "updated": "Aktualisierte",
13 | "noFilesFound": "Keine Dateien gefunden",
14 | "untitledFolder": "Unbenannter Ordner",
15 | "search": "Suche",
16 | "sizes": {
17 | "b": "{fileSize} B",
18 | "kb": "{fileSize} KB",
19 | "mb": "{fileSize} MB",
20 | "gb": "{fileSize} GB"
21 | }
22 | },
23 | "accounts": {
24 | "confirmRemove": "Sie können auf dieses Konto nicht mehr sehen. Sind Sie sicher, Sie möchten es entfernen? (Beachten Sie, dass diese Sie nicht melden Sie sich von dem Konto auf diesem Browser)",
25 | "connectedAccounts": "Verbundene Konten",
26 | "logout": "Ausloggen",
27 | "manage": "Verwalten Sie hier Ihre Cloud-Speicherkonten.",
28 | "chooseAccount": "Herzlich willkommen! Bitte wählen Sie einen Dienst aus, um eine Verbindung herzustellen",
29 | "connectMore": "Verbinde mehr!",
30 | "upload": "Hochladen",
31 | "uploadFromComputer": "Hochladen von Ihrem Computer"
32 | },
33 | "selector": {
34 | "myComputer": "Mein Computer",
35 | "accounts": "Konten"
36 | },
37 | "dropzone": {
38 | "message": "Ziehen Sie Dateien hierher, und legen Sie sie dort ab, oder klicken Sie darauf, um die Dateiauswahl zu öffnen"
39 | },
40 | "computer": {
41 | "noSupport": "Ihr Browser unterstützt Flash, Silverlight oder HTML5 nicht."
42 | },
43 | "addConfirm": {
44 | "confirm": "Bestätigen Sie die Kontoverbindung.",
45 | "clickBelow": "Klicken Sie unten, um eine Verbindung zu Ihrem {serviceName} Konto herzustellen",
46 | "connectAccount": "{ServiceName} Konto verbinden"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/picker/js/iexd-transport.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import $ from 'jquery';
3 | import config from './config';
4 | import auth from './auth';
5 | import util from './util';
6 |
7 | /**
8 | * Compatability module which proxies requests to the Kloudless API via
9 | * iexd.html (hosted on the API domain) to avoid IE9 restrictions on
10 | * cross-domain requests.
11 | *
12 | * Adds a jQuery AJAX transport which handles all requests to the API server.
13 | *
14 | * In particular, IE9 has some limitations on use of XDomainRequest that
15 | * prevent us from using it to talk to the API server:
16 | * - can't send custom headers like Authorization
17 | * - protocol of the page needs to be https (protocols must match)
18 | * - only GET and POST methods can be used
19 | * - can only use text/plain for the request's Content-Type
20 | */
21 | 'use strict';
22 |
23 | // do nothing if proper cross-domain requests are supported
24 |
25 | if (!util.supportsCORS()) {
26 | $.ajaxTransport('+*', function (options, originalOptions, jqXHR) {
27 | if (!canHandleRequest(options)) {
28 | return;
29 | }
30 |
31 | return {
32 | send: function (headers, completeCallback) {
33 | var requestId = util.randomID();
34 | options.headers = headers;
35 |
36 | var data = {
37 | type: 'proxy',
38 | origin: document.URL,
39 | id: requestId,
40 | options: options
41 | };
42 |
43 | auth.postMessage(data, requestId, function (response) {
44 | completeCallback(
45 | response.status, response.statusText, response.responses, response.headers);
46 | });
47 | },
48 |
49 | /* we don't support aborting requests on IE9 :-) */
50 | abort: function () {
51 | }
52 | };
53 | });
54 | }
55 |
56 | function canHandleRequest(options) {
57 | return options.crossDomain &&
58 | options.url.substring(0, config.base_url.length) == config.base_url;
59 | }
60 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'stylelint-config-standard',
4 | ],
5 | plugins: [
6 | 'stylelint-order',
7 | ],
8 | rules: {
9 | 'order/properties-order': [
10 | 'position',
11 | 'top',
12 | 'bottom',
13 | 'right',
14 | 'left',
15 | 'display',
16 | 'align-items',
17 | 'justify-content',
18 | 'float',
19 | 'clear',
20 | 'overflow',
21 | 'overflow-x',
22 | 'overflow-y',
23 | 'margin',
24 | 'margin-top',
25 | 'margin-right',
26 | 'margin-bottom',
27 | 'margin-left',
28 | 'padding',
29 | 'padding-top',
30 | 'padding-right',
31 | 'padding-bottom',
32 | 'padding-left',
33 | 'width',
34 | 'min-width',
35 | 'max-width',
36 | 'height',
37 | 'min-height',
38 | 'max-height',
39 | 'font-size',
40 | 'font-family',
41 | 'font-weight',
42 | 'text-align',
43 | 'text-justify',
44 | 'text-indent',
45 | 'text-overflow',
46 | 'text-decoration',
47 | 'white-space',
48 | 'color',
49 | 'background',
50 | 'background-position',
51 | 'background-repeat',
52 | 'background-size',
53 | 'background-color',
54 | 'background-clip',
55 | 'border',
56 | 'border-style',
57 | 'border-width',
58 | 'border-color',
59 | 'border-top-style',
60 | 'border-top-width',
61 | 'border-top-color',
62 | 'border-right-style',
63 | 'border-right-width',
64 | 'border-right-color',
65 | 'border-bottom-style',
66 | 'border-bottom-width',
67 | 'border-bottom-color',
68 | 'border-left-style',
69 | 'border-left-width',
70 | 'border-left-color',
71 | 'border-radius',
72 | 'opacity',
73 | 'filter',
74 | 'list-style',
75 | 'outline',
76 | 'visibility',
77 | 'z-index',
78 | 'box-shadow',
79 | 'text-shadow',
80 | 'resize',
81 | 'transition',
82 | ],
83 | },
84 | };
85 |
--------------------------------------------------------------------------------
/src/picker/templates/breadcrumb.pug:
--------------------------------------------------------------------------------
1 | .breadcrumb(data-bind='with: files')
2 | .dropdown(id='breadcrumb-dropdown')
3 | .box.box--shadow
4 | .box__section
5 | .list
6 | .list__item(data-bind='click: up.bind(null, breadcrumbs().length)')
7 | .icon
8 | .icon__folder
9 | .list__text(data-bind='text: root_folder_name')
10 | // ko foreach: breadcrumbs
11 | .list__item(data-bind='click: $parent.up.bind(null, $parent.breadcrumbs().length - parseInt("" + ($index() + 1), 10))')
12 | .icon
13 | .icon__folder
14 | .list__text(data-bind='text: $data.path')
15 | // ko if: $parent.breadcrumbs().length -1 === $index()
16 | .icon.icon--small
17 | .icon__checked
18 | // /ko
19 | // /ko
20 | //- breadcrumb__hidden is a hidden element which is used to measure the
21 | //- breadcrumb width and decide whether to ellipsis breadcrumb
22 | .breadcrumb__hidden
23 | .icon.icon--button.icon--large(data-bind=`
24 | attr: {'title': root_folder_name},
25 | click: up.bind(null, breadcrumbs().length)`)
26 | .icon__root-dir
27 | .breadcrumb__toggle-btn(
28 | data-bind="css: {'breadcrumb__toggle-btn--hidden': breadcrumbs().filter(function(e) { return !e.visible}).length === 0}")
29 | .icon
30 | .icon__next
31 | .breadcrumb__text.breadcrumb__text--button(
32 | data-dropdown-id='breadcrumb-dropdown')
33 | | ...
34 | // ko foreach: breadcrumbs
35 | // ko if: $data.visible === true
36 | .icon
37 | .icon__next
38 | .breadcrumb__text(data-bind=`
39 | attr: {'title': $data.path},
40 | css: {
41 | 'breadcrumb__text--button': $parent.breadcrumbs().length -1 !== $index(),
42 | 'breadcrumb__text--ellipsis': $parent.breadcrumbs().filter(function(e) { return e.visible }).length === 1 || $parent.breadcrumbs().length -1 !== $index(),
43 | },
44 | click: $parent.up.bind(null, $parent.breadcrumbs().length - parseInt("" + ($index() + 1), 10)),
45 | text: $data.path`)
46 | // /ko
47 | // /ko
48 |
--------------------------------------------------------------------------------
/storybook-test/stories/core/components.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function TextArea(props) {
4 | const {
5 | value, name, title, onChange, error,
6 | } = props;
7 | const outerClass = [
8 | 'mdl-textfield mdl-textfield--floating-label',
9 | // Somehow MDL doesn't detect the value while switching between stories.
10 | // So add is-dirty class manually.
11 | value ? 'is-dirty' : '',
12 | error ? 'is-invalid' : '',
13 | ].join(' ');
14 | return (
15 |
16 |
25 | );
26 | }
27 |
28 |
29 | function TextInput(props) {
30 | const {
31 | value, name, title, onChange, error, type = 'text',
32 | } = props;
33 | const outerClass = [
34 | 'mdl-textfield mdl-textfield--floating-label',
35 | // Somehow MDL doesn't detect the value while switching between stories.
36 | // So add is-dirty class manually.
37 | value ? 'is-dirty' : '',
38 | error ? 'is-invalid' : '',
39 | ].join(' ');
40 | return (
41 |
42 | onChange(name, e)} />
46 |
47 | {error && {error}}
48 |
49 | );
50 | }
51 |
52 | function Grid(props) {
53 | return ({props.children}
);
54 | }
55 |
56 | function GridCell(props) {
57 | const { children } = props;
58 | return (
59 |
60 | {children}
61 |
62 | );
63 | }
64 |
65 | export {
66 | TextInput, Grid, GridCell, TextArea,
67 | };
68 |
--------------------------------------------------------------------------------
/config/story-dev-server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Express server for building and hosting the loader and picker page in dev
3 | * env.
4 | */
5 | /* eslint-disable no-console */
6 | const express = require('express');
7 | const webpack = require('webpack');
8 | const webpackDevMiddleware = require('webpack-dev-middleware');
9 | const webpackHotMiddleware = require('webpack-hot-middleware');
10 | const { devServerPorts } = require('./common');
11 |
12 | // BUILD_LICENSE defaults to AGPL.
13 | // If it's not AGPL then some tests or stories may fail due to plupload
14 | // being disabled.
15 | if (!process.env.BUILD_LICENSE) {
16 | process.env.BUILD_LICENSE = 'AGPL';
17 | }
18 |
19 | const webpackDevConfig = require('./webpack.story.conf');
20 |
21 | const app = express();
22 | const compiler = webpack(webpackDevConfig);
23 |
24 | console.log('Starting Dev Server...');
25 |
26 | // Accept HMR requests from storybook server.
27 | app.use((req, res, next) => {
28 | res.append('Access-Control-Allow-Origin', ['*']);
29 | res.append('Access-Control-Allow-Methods', 'GET');
30 | next();
31 | });
32 |
33 | // bind webpack and hot reload
34 | app.use(webpackDevMiddleware(compiler, {
35 | logTime: true,
36 | stats: 'minimal',
37 | publicPath: '/',
38 | }));
39 | app.use(webpackHotMiddleware(compiler, {
40 | log: console.log,
41 | path: '/__webpack_hmr',
42 | heartbeat: 10 * 1000,
43 | }));
44 |
45 | // Listen on 2 ports to host loader and picker separately.
46 | app.listen(devServerPorts.loader, () => {
47 | console.log(
48 | 'Loader hosted at '
49 | + `http://localhost:${devServerPorts.loader}/sdk/kloudless.picker.js`,
50 | );
51 | console.log(
52 | 'React binding hosted at '
53 | + `http://localhost:${devServerPorts.loader}/sdk/kloudless.picker.react.js`,
54 | );
55 | console.log(
56 | 'Vue binding hosted at '
57 | + `http://localhost:${devServerPorts.loader}/sdk/kloudless.picker.vue.js`,
58 | );
59 | console.log('Webpack bundles are compiling...');
60 | });
61 | app.listen(devServerPorts.picker, () => {
62 | console.log(
63 | 'View page hosted at '
64 | + `http://localhost:${devServerPorts.picker}/file-picker/v2/index.html`,
65 | );
66 | console.log('Webpack bundles are compiling...');
67 | });
68 |
--------------------------------------------------------------------------------
/src/picker/css/izitoast.less:
--------------------------------------------------------------------------------
1 | // override styles in izitoast/dist/css/iziToast.css
2 | .iziToast {
3 | // to allow user to select text
4 | -webkit-touch-callout: default; // iOS Safari
5 | user-select: text;
6 | cursor: text;
7 |
8 | // override other styles
9 | max-width: 100%;
10 | border-radius: @border_radius !important;
11 | box-shadow: @dialog_shadow_color 0 8px 20px;
12 |
13 | &::after {
14 | box-shadow: none !important;
15 | }
16 | }
17 |
18 | // override styles in izitoast/dist/css/iziToast.css
19 | .iziToast-overlay {
20 | background-color: @dialog_overlay_color !important;
21 | }
22 |
23 | // override styles in izitoast/dist/css/iziToast.css
24 | .iziToast-texts {
25 | overflow-wrap: break-word;
26 | }
27 |
28 | .iziToast-theme-custom-success {
29 | background-color: @dialog_success_bg_color !important;
30 |
31 | .iziToast-title {
32 | color: @dialog_success_title_color !important;
33 | }
34 |
35 | .iziToast-message {
36 | color: @dialog_success_message_color !important;
37 | }
38 |
39 | .iziToast-icon {
40 | color: @dialog_success_icon_color !important;
41 | }
42 | }
43 |
44 | .iziToast-theme-custom-error {
45 | background-color: @dialog_error_bg_color !important;
46 |
47 | .iziToast-title {
48 | color: @dialog_error_title_color !important;
49 | }
50 |
51 | .iziToast-icon {
52 | color: @dialog_error_icon_color !important;
53 | }
54 |
55 | .iziToast-message {
56 | color: @dialog_error_message_color !important;
57 | }
58 | }
59 |
60 | .iziToast__details-toggle-btn {
61 | cursor: pointer;
62 |
63 | // disable border highlight when focus
64 | &:focus {
65 | outline: none;
66 | }
67 | }
68 |
69 | .iziToast__ok-btn {
70 | color: @primary_btn_text_color !important;
71 | background-color: @primary_btn_color !important;
72 | border-radius: @border_radius;
73 |
74 | &:hover {
75 | background-color: @primary_btn_hover_color !important;
76 | }
77 | }
78 |
79 | .iziToast__copy-btn {
80 | color: @secondary_btn_text_color !important;
81 | background-color: @secondary_btn_color !important;
82 | border: 1px solid @dialog_btn_border_color !important;
83 | border-radius: @border_radius !important;
84 |
85 | &:hover {
86 | background-color: @secondary_btn_hover_color !important;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/dev-server/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIGOTCCBCGgAwIBAgIJAMRfJgV4LXGqMA0GCSqGSIb3DQEBDQUAMHAxCzAJBgNV
3 | BAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMREwDwYDVQQHEwhCZXJrZWxleTEY
4 | MBYGA1UEChMPS2xvdWRsZXNzLCBJbmMuMQswCQYDVQQLEwJJVDESMBAGA1UEAxMJ
5 | bG9jYWxob3N0MB4XDTE3MDYwNTA5MzExNVoXDTI3MDYwMzA5MzExNVowcDELMAkG
6 | A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCEJlcmtlbGV5
7 | MRgwFgYDVQQKEw9LbG91ZGxlc3MsIEluYy4xCzAJBgNVBAsTAklUMRIwEAYDVQQD
8 | Ewlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJikms
9 | rakJOv1sYPlLWXdK7FeMwronbRDsPPMcaGTXq2jqkNwCaGG5nmwTp4dOBwx/BZ3I
10 | S/4Di0kVOhu7kbM6/qjFmh7gr3gKJNw2jDwMF5JqeyPAK+3onLaGcTzNxhDg4vw4
11 | ouQ3eUF5vK9XaIP3RjyiRSseIdg2psa3UwZ/fZlJf4u9co2Yb00tkkd5L7tdCb+Q
12 | 001S7R2EbqHFi8izf0SQ3t50/SfTiynrz0r7zB04HJ1LvwcSVzfCR3WT732cRPp9
13 | ibSQOou/IPGNYAjXWYpxa61Re8oBYc79P0SA5ONKXbIX335Avb2hwT093nF/uDkX
14 | POlDfxoUOwQEjRDQ+v7NEZV8eFxmDCfer49C3aVNnx6s3ohdqbS7tDnh2Km+rpul
15 | sujog2E3rhFRUtxdXw85tmbqN7kC4pHHoQ2b7nFxL5rD22/S94cjqDBb40npv+3Q
16 | KNkTzdhy3gjaDKk//1fWp2Nl1a30N3UJ5F9oxvs+L/11+LT37uCi6Dol/7Q2kJzq
17 | eLpvf6AwoqhyTf1Lbdp624JhYxqfj1I6RLfeuImLASsXcCgFTuVa3BcjvXdgmKFZ
18 | RdbQbFLWeR/Pnn7yng4N0RVyIQf6DGtcwBTiUQj0mdl6Hng/B3ZWvcSU9uudt6s1
19 | 08maFyfWZ4vkP+mvgNOcBpx0+r7omdshrhkgYQIDAQABo4HVMIHSMB0GA1UdDgQW
20 | BBTlx3q6mi5PwCalGqaOJRQ/zqT8vjCBogYDVR0jBIGaMIGXgBTlx3q6mi5PwCal
21 | GqaOJRQ/zqT8vqF0pHIwcDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3Ju
22 | aWExETAPBgNVBAcTCEJlcmtlbGV5MRgwFgYDVQQKEw9LbG91ZGxlc3MsIEluYy4x
23 | CzAJBgNVBAsTAklUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQDEXyYFeC1xqjAMBgNV
24 | HRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4ICAQAmZcD9KTVYBOOd2mRe4LE5z4yP
25 | a36h18ohdKux8F1n1mAwOGI+I3trdWVzX9fZfIBdz5U+s6fHPetkfWsmBhcMDa3W
26 | HtKvEIgPBZLMtEKsLPE5gqRg10Yi3skwPHV6kdSr6LdOLxXKOLpMz7ArfY4szbJ/
27 | 4ySNRik4xwpA7L8Lu4sEhJHrTVOZvkVQ1fpfWo7f8X/HuhFji8Sg8x0NfJU29AnK
28 | VpQcuEwsxOO1rsB08Dh0GtGJsDDJ1pYv/IWuI2jky7rvA2QXyiDUlNepHp3oRVNw
29 | YQPJjZ7JkxHeznKkrLDgzg4hWtrJwSwKBSE68PoolmVu9KFLEzw/eNz8li5ssroD
30 | ja3C9y/UVC7gVw4J0iG2mV3rxbK2F8BBk2gClGUTL4ct7OruK7352pqcf9lQG6yQ
31 | nloMOuzoimcWbK1ix5WQHgledrq9IfiHs5RJzeOmYpRLC24l9X/EsP/ydwPzk64w
32 | S7gnID90oaejE2tn5GTmixrAzUjqLd7lwc9eu22JnRKXn28iP3afZhiJATaMLcEz
33 | gC6+OsQ3bHizZbgD4OWgZKXQvAPtWeR+QQsog2Drj4ThfYgMNyRceFUYsBnjbmYn
34 | 6E50S490nEkBOjXb655muVmfV/8tc1qP4Qe9OTKrliBVW4j8e/jc2/sD9YOO2Lt6
35 | vlpo0FuNnp/PH60inw==
36 | -----END CERTIFICATE-----
37 |
--------------------------------------------------------------------------------
/src/picker/css/util.less:
--------------------------------------------------------------------------------
1 | .flex-row {
2 | display: flex;
3 | }
4 |
5 | .align-items-center {
6 | align-items: center;
7 | }
8 |
9 | .flex-col {
10 | display: flex;
11 | flex-direction: column;
12 | }
13 |
14 | .justify-content-center {
15 | justify-content: center;
16 | }
17 |
18 | .flex-no-shrink {
19 | flex-shrink: 0;
20 | min-width: 0;
21 | }
22 |
23 | .flex-grow {
24 | flex-grow: 1;
25 | min-width: 0;
26 | min-height: 0;
27 | }
28 |
29 | .pl {
30 | padding-left: 12px;
31 | }
32 |
33 | .pr {
34 | padding-right: 12px;
35 | }
36 |
37 | .ml {
38 | margin-left: 12px;
39 | }
40 |
41 | .mr {
42 | margin-right: 12px;
43 | }
44 |
45 | .mt {
46 | margin-top: 12px;
47 | }
48 |
49 | .mb {
50 | margin-bottom: 12px;
51 | }
52 |
53 | .invisible {
54 | visibility: hidden;
55 | }
56 |
57 | .visual-invisible {
58 | // for hiding temp element
59 | // note: setting display to `none` won't work with execCommand
60 | // originated from https://getbootstrap.com/docs/4.0/utilities/screenreaders/
61 | position: absolute;
62 | top: 0;
63 | left: 0;
64 | overflow: hidden;
65 | padding: 0;
66 | width: 1px;
67 | height: 1px;
68 | clip: rect(0, 0, 0, 0);
69 | white-space: nowrap;
70 | border: 0;
71 | }
72 |
73 | .hidden {
74 | display: none;
75 | }
76 |
77 | .sm-hidden {
78 | @media only screen and (max-width: @BREAKING_POINT) {
79 | display: none;
80 | }
81 | }
82 |
83 | .md-hidden {
84 | @media only screen and (min-width: @BREAKING_POINT) {
85 | display: none;
86 | }
87 |
88 | // Revert display when width is exactly = @BREAKING_POINT
89 | // Note: not all elements' initial value of display is 'block'.
90 | // Here we only apply md-hidden to tag. So 'block' is fine.
91 | // Be careful when applying md-hidden to HTML tags whose initial
92 | // display value is not 'block'.
93 | @media only screen and (width: @BREAKING_POINT) {
94 | display: block;
95 | }
96 | }
97 |
98 | .text-center {
99 | text-align: center;
100 | }
101 |
102 | .h-100 {
103 | height: 100%;
104 | }
105 |
106 | .w-100 {
107 | width: 100%;
108 | }
109 |
110 | // Used to fix the table wrap issue in IE
111 | .ie-fix-wrap {
112 | .word-break();
113 |
114 | flex: 1 1 300px;
115 | width: 100%;
116 | }
117 |
--------------------------------------------------------------------------------
/storybook-react/stories/helpers.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import {
4 | object, text, boolean, number,
5 | } from '@storybook/addon-knobs';
6 | import {
7 | DROPZONE_EVENT_HANDLER_MAPPING,
8 | EVENT_HANDLER_MAPPING,
9 | } from '../../src/loader/js/react/constants';
10 |
11 | const APP_ID = (process.env.STORYBOOK_KLOUDLESS_APP_ID
12 | || 'J2hLI4uR9Oj9_UiJ2Nnvhj9k1SxlZDG3xMtAQjvARvgrr3ie');
13 |
14 | function genOptions(name) {
15 | const options = { app_id: APP_ID };
16 | if (name === 'Saver' || name === 'createSaver') {
17 | options.files = [
18 | {
19 | url: 'https://s3-us-west-2.amazonaws.com/static-assets.kloudless.com/logo_mark.png',
20 | name: 'kloudless-logo.png',
21 | },
22 | ];
23 | }
24 | return object(`(${name}) options`, options, name);
25 | }
26 |
27 | function genEventHandlers(name) {
28 | const eventHandlerMapping = name === 'Dropzone'
29 | ? DROPZONE_EVENT_HANDLER_MAPPING : {
30 | ...EVENT_HANDLER_MAPPING,
31 | click: 'onClick',
32 | };
33 | return Object.keys(eventHandlerMapping).reduce((result, event) => {
34 | const eventHandler = eventHandlerMapping[event];
35 | result[eventHandler] = (...args) => {
36 | console.log(`(${name}) ${eventHandler} called:`);
37 | console.dir(args);
38 | };
39 | return result;
40 | }, {});
41 | }
42 |
43 | export function GreenButton({ onClick }) {
44 | return (
45 |
52 | );
53 | }
54 |
55 | export function genProps(name) {
56 | const props = {
57 | options: genOptions(name),
58 | ...genEventHandlers(name),
59 | };
60 | if (name === 'Saver' || name === 'Chooser') {
61 | props.className = text(
62 | `(${name}) className`, 'btn btn-outline-dark', name,
63 | );
64 | props.disabled = boolean(`(${name}) disabled`, false, name);
65 | props.title = text(
66 | `(${name}) title`,
67 | name === 'Saver' ? 'test saver' : 'test chooser',
68 | name,
69 | );
70 | } else if (name === 'Dropzone') {
71 | props.height = number(`(${name}) height`, 100, {}, name);
72 | props.width = number(`(${name}) width`, 600, {}, name);
73 | }
74 | return props;
75 | }
76 |
--------------------------------------------------------------------------------
/src/loader/js/react/Dropzone.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import filePicker from '../interface';
4 | import {
5 | DROPZONE_EVENT_HANDLER_MAPPING,
6 | DROPZONE_EVENT_HANDLERS,
7 | } from './constants';
8 |
9 | class Dropzone extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.onRaw = this.onRaw.bind(this);
13 | this.dropzone = null;
14 | this.elementId = `dz-${Math.floor(Math.random() * (10 ** 12))}`;
15 | this.initDropzone = this.initDropzone.bind(this);
16 | }
17 |
18 | componentDidMount() {
19 | const { options } = this.props;
20 | this.initDropzone(options);
21 | }
22 |
23 | componentWillReceiveProps(nextProps) {
24 | const { options } = this.props;
25 | if (options !== nextProps.options) {
26 | this.dropzone.destroy();
27 | this.initDropzone(nextProps.options);
28 | }
29 | }
30 |
31 | componentWillUnmount() {
32 | this.dropzone.destroy();
33 | }
34 |
35 | onRaw(event, ...args) {
36 | const eventHandler = DROPZONE_EVENT_HANDLER_MAPPING[event];
37 | if (typeof this.props[eventHandler] === 'function') {
38 | this.props[eventHandler](...args);
39 | }
40 | }
41 |
42 | initDropzone(options) {
43 | this.dropzone = filePicker.dropzone({
44 | ...options,
45 | elementId: this.elementId,
46 | });
47 | this.dropzone.on('raw', this.onRaw);
48 | }
49 |
50 | render() {
51 | const { height, width } = this.props;
52 | return (
53 |
57 | );
58 | }
59 | }
60 |
61 | // set prop type
62 | Dropzone.propTypes = {
63 | ...DROPZONE_EVENT_HANDLERS.reduce((result, key) => {
64 | result[key] = PropTypes.func;
65 | return result;
66 | }, {}),
67 | options: PropTypes.shape({
68 | app_id: PropTypes.string.isRequired,
69 | }).isRequired,
70 | onSuccess: PropTypes.func.isRequired, // eslint-disable-line
71 | height: PropTypes.number,
72 | width: PropTypes.number,
73 | };
74 |
75 | // set default props
76 | Dropzone.defaultProps = {
77 | ...DROPZONE_EVENT_HANDLERS.reduce((result, key) => {
78 | result[key] = () => { };
79 | return result;
80 | }, {}),
81 | height: 100,
82 | width: 600,
83 | };
84 |
85 | export default Dropzone;
86 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | // TODO: send events with the below constants instead of hard coded string
2 |
3 | export const EVENTS = {
4 | SUCCESS: 'success',
5 | CANCEL: 'cancel',
6 | ERROR: 'error',
7 | OPEN: 'open',
8 | LOAD: 'load',
9 | CLOSE: 'close',
10 | SELECTED: 'selected',
11 | ADD_ACCOUNT: 'addAccount',
12 | DELETE_ACCOUNT: 'deleteAccount',
13 | START_FILE_UPLOAD: 'startFileUpload',
14 | FINISH_FILE_UPLOAD: 'finishFileUpload',
15 | LOGOUT: 'logout',
16 | DROP: 'drop',
17 | };
18 |
19 | export const EVENTS_LIST = Object.values(EVENTS);
20 |
21 | export const VIEW_EVENTS = {
22 | DROPZONE_CLICKED: 'dropzoneClicked',
23 | GET_OAUTH_PARAMS: 'GET_OAUTH_PARAMS',
24 | INIT_CLOSE: 'INIT_CLOSE',
25 | };
26 |
27 | export const LOADER_INTERNAL_EVENTS = {
28 | DATA: 'DATA',
29 | INIT: 'INIT',
30 | LOGOUT: 'LOGOUT',
31 | LOGOUT_DELETE_ACCOUNT: 'LOGOUT:DELETE_ACCOUNT',
32 | CALLBACK: 'CALLBACK',
33 | CLOSING: 'CLOSING',
34 | };
35 |
36 | /**
37 | * A map to list out loader features and minimum supported version
38 | * Warning: In general, we should avoid doing b/c incompatible changes as
39 | * there might be apps using older version of loaders.
40 | * Extend this map only when necessary.
41 | *
42 | * MAP FORMAT: {[FEATURE NAME]: [LAST UNSUPPORTED VERSION]}
43 | *
44 | * Record "last unsupported version" because we only know the next version
45 | * number before rollout.
46 | */
47 | export const LOADER_FEATURES = {
48 | /**
49 | * loader can handle unknown events, older versions don't handle
50 | * unknown events well, sending them could cause script errors
51 | */
52 | CAN_HANDLE_UNKNOWN_EVENTS: '2.5.1',
53 |
54 | // Include folders in selected event data.
55 | CAN_INCLUDE_FOLDERS_IN_SELECTED_EVENT_DATA: '2.5.4',
56 |
57 | // In previous version, File Picker's Computer View still fires success event
58 | // when cancel or no items succeed while other modes behave differently.
59 | // We change to not fire success event on cancel or all items fail.
60 | COMPUTER_NO_SUCCESS_ON_CANCEL_OR_FAIL: '2.5.4',
61 | };
62 |
63 | export const LOADER_E2E_SELECTORS = {
64 | // class name
65 | J_LAUNCH_BTN: 'j-launch-btn',
66 | // id
67 | IFRAME_NAME: 'kloudless-file-picker-iframe',
68 | };
69 |
70 | export const SORTING = {
71 | DIRECTION: {
72 | ASC: 'asc',
73 | DESC: 'desc',
74 | },
75 | };
76 |
--------------------------------------------------------------------------------
/src/picker/templates/selector.pug:
--------------------------------------------------------------------------------
1 | //- Select account
2 | .account-selector(data-bind='with: accounts', data-dropdown-id='account-dropdown')
3 | .account-selector__button
4 | .icon
5 | // ko if: $root.selectingFilesFrom().fromComputer
6 | .icon__computer
7 | // /ko
8 | // ko ifnot: $root.selectingFilesFrom().fromComputer
9 | img.icon__service(data-bind='attr: {src: active_logo()}')
10 | // /ko
11 | .account-selector__name(data-bind='text: name')
12 | .account-selector__arrow
13 | .dropdown(id='account-dropdown')
14 | .box.box--shadow
15 | .box__title(data-bind='translate: "selector/selectFrom"')
16 | .box__section
17 | .list
18 | .list__item.list__item--current
19 | .icon
20 | // ko if: $root.selectingFilesFrom().fromComputer
21 | .icon__computer
22 | // /ko
23 | // ko ifnot: $root.selectingFilesFrom().fromComputer
24 | img.icon__service(data-bind="attr: {src: $root.selectingFilesFrom().icon}")
25 | // /ko
26 | .list__text(data-bind='text: $root.selectingFilesFrom().text')
27 | .icon.icon--small
28 | .icon__checked
29 | .box__divider
30 | .box__title(data-bind='translate: "selector/switchAccount"')
31 | .box__section
32 | .list
33 | // ko foreach: $root.filteredAccounts
34 | // ko if: $root.services()[service]
35 | .list__item(data-bind='attr: {title: $root.services()[service].name + ": " + account_name}, click: $root.setLocation.bind($data, "#/files/" + account)')
36 | .icon
37 | img.icon__service(data-bind='attr: {src: $root.services()[service].logo}')
38 | .list__text(data-bind='text: account_name')
39 | // /ko
40 | // /ko
41 | // ko if: computer() && !$root.selectingFilesFrom().fromComputer
42 | .list__item(data-bind='click: $root.setLocation.bind($data, "#/computer")')
43 | .icon
44 | .icon__computer
45 | .list__text(data-bind='translate: "selector/myComputer"')
46 | // /ko
47 | // ko if: account_management()
48 | .list__item(data-bind='click: $root.setLocation.bind($data, "#/accounts")')
49 | .icon
50 | .icon__accounts
51 | .list__text(data-bind='translate: "selector/accounts"')
52 | // /ko
53 |
--------------------------------------------------------------------------------
/src/picker/css/filetable.less:
--------------------------------------------------------------------------------
1 | .ftable {
2 | flex: 1 1 auto;
3 | display: flex;
4 | flex-direction: column;
5 | width: 100%;
6 | height: 100%;
7 | min-height: 0;
8 | background-color: @section_primary_bg_color;
9 | border-radius: @border_radius;
10 | }
11 |
12 | .ftable__main {
13 | flex: 1 1 auto;
14 | overflow-y: auto;
15 | padding: 0 16px;
16 | height: 100%;
17 | min-height: 0;
18 | border: solid 1px @section_line_color;
19 | }
20 |
21 | .ftable__table {
22 | position: relative;
23 | }
24 |
25 | .ftable__th {
26 | cursor: pointer;
27 | position: sticky;
28 | top: 0;
29 | font-size: @font_size_md;
30 | background-color: @section_primary_bg_color;
31 | }
32 |
33 | .ftable__no-data-cell {
34 | text-align: center;
35 | border: 0;
36 | }
37 |
38 | .ftable__row {
39 | cursor: pointer;
40 |
41 | &:hover {
42 | background-color: @section_primary_hover_bg_color;
43 | }
44 | }
45 |
46 | // applied for new loaded file and folder row
47 | .ftable__row--new-loaded {
48 | color: @new_loaded_text_color;
49 | background-color: @new_loaded_bg_color;
50 | }
51 |
52 | // applied for selected file and folder row
53 | .ftable__row--selected {
54 | color: @selected_text_color;
55 | background-color: @selected_bg_color;
56 |
57 | &:hover {
58 | background-color: @selected_hover_bg_color;
59 | }
60 | }
61 |
62 | // applied for disabled file row
63 | .ftable__row--disabled {
64 | cursor: unset;
65 | color: @disabled_text_color;
66 | background-color: @section_primary_bg_color;
67 |
68 | &:hover {
69 | background-color: @section_primary_bg_color;
70 | }
71 | }
72 |
73 | .ftable__row--disabled.ftable__row--new-loaded {
74 | cursor: unset;
75 | background-color: @new_loaded_bg_color;
76 | &:hover {
77 | background-color: @new_loaded_bg_color;
78 | }
79 | }
80 |
81 | // applied when clicking on a disabled folder row
82 | .ftable__row--focus {
83 | .ftable__row--selected();
84 | }
85 |
86 | .ftable__cell-md {
87 | font-size: @font_size_md;
88 | }
89 |
90 | .ftable__footer {
91 | padding: 12px 0;
92 | flex: 0 0 auto;
93 | background-color: @section_secondary_bg_color;
94 | }
95 |
96 | .ftable__saver-input {
97 | padding: 0 12px;
98 | width: 100%;
99 | height: 40px;
100 | background-color: @input_color;
101 | border: solid 1px @input_border_color;
102 | border-radius: @border_radius;
103 | }
104 |
--------------------------------------------------------------------------------
/config/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | /* Webpack config for dev-server */
3 | const path = require('path');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const webpack = require('webpack');
6 | const getPickerPlugins = require('./picker-plugins');
7 | const baseWebpackConfig = require('./webpack.base.conf');
8 | const merge = require('./merge-strategy');
9 |
10 | const devServerContentPath = path.resolve(__dirname, '../dev-server/');
11 |
12 |
13 | const devConfigBase = merge(baseWebpackConfig, {
14 | mode: 'development',
15 | devtool: '#source-map',
16 | output: {
17 | path: path.resolve(devServerContentPath, 'dist'),
18 | filename: '[name].js',
19 | // must be '/' here for HtmlWebpackPlugin to inject correct resource path
20 | // https://github.com/jantimon/html-webpack-plugin/issues/1009
21 | publicPath: '/',
22 | },
23 | plugins: [
24 | new webpack.HotModuleReplacementPlugin(),
25 | ],
26 | });
27 |
28 | module.exports = [
29 | // test page and loader
30 | merge(devConfigBase, {
31 | entry: {
32 | index: './dev-server/index.js',
33 | 'loader/loader': './config/loader-export-helper.js',
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin({
37 | filename: 'index.html',
38 | template: path.resolve(devServerContentPath, 'index.ejs'),
39 | templateParameters: {
40 | distTest: false,
41 | },
42 | chunks: ['index'],
43 | }),
44 | ],
45 | }),
46 | // picker
47 | merge(devConfigBase, {
48 | entry: {
49 | 'picker/picker': [
50 | 'webpack-hot-middleware/client?quiet=true',
51 | './src/picker/js/app.js',
52 | ],
53 | 'picker/template-hot-loader': [
54 | 'webpack-hot-middleware/client?quiet=true',
55 | './dev-server/picker-template-hot-loader.js',
56 | ],
57 | },
58 | plugins: [
59 | // picker page
60 | new HtmlWebpackPlugin({
61 | filename: path.resolve(
62 | devServerContentPath, 'dist/picker/index.html',
63 | ),
64 | template: path.resolve(
65 | __dirname,
66 | '../src/picker/templates/index.pug',
67 | ),
68 | chunks: ['picker/picker', 'picker/template-hot-loader'],
69 | }),
70 | // Don't watch static json files to reduce CPU usage
71 | new webpack.WatchIgnorePlugin([/bower_components\/cldr-data/]),
72 | ].concat(getPickerPlugins(
73 | path.join(devServerContentPath, 'dist/picker/'),
74 | )),
75 | }),
76 | ];
77 |
--------------------------------------------------------------------------------
/dev-server/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <% if (distTest) { %>
7 |
8 | <% } else { %>
9 |
10 | <% } %>
11 | Kloudless File Picker Test Page
12 |
13 |
14 |
15 | Click below to test the File Picker's Chooser and Saver modes.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Drop Zone
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 | <% if (distTest) { %>
40 |
41 |
43 |
54 |
55 | <% } else { %>
56 |
57 |
59 |
63 | <% } %>
64 |
65 |
66 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/core/folder_content_response.json:
--------------------------------------------------------------------------------
1 | {
2 | "page": 1,
3 | "count": 2,
4 | "next_page": null,
5 | "objects": [
6 | {
7 | "id": "folder_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRExhQQ",
8 | "created": null,
9 | "modified": null,
10 | "size": null,
11 | "path": "/very-very-very-very-very-very-long-folder-name",
12 | "ancestors": [
13 | {
14 | "type": "folder",
15 | "id": "root",
16 | "name": "Dropbox",
17 | "id_type": "path"
18 | }
19 | ],
20 | "parent": {
21 | "type": "folder",
22 | "id": "root",
23 | "name": "Dropbox",
24 | "id_type": "path"
25 | },
26 | "account": 1234567890,
27 | "ids": {
28 | "default": "folder_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRExhQQ",
29 | "path": "folder_L3dlZGRpbmctc2NyZWVu"
30 | },
31 | "id_type": "default",
32 | "api": "storage",
33 | "name": "very-very-very-very-very-very-very-very-very-long-folder-name",
34 | "type": "folder",
35 | "can_create_folders": true,
36 | "can_upload_files": true,
37 | "can_list_recursively": true,
38 | "href": "https://api.kloudless.com/v1/accounts/1234567890/storage/folders/folder_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRExhQQ",
39 | "raw_id": "id:6e7mtBeH4sAAAAAAAADLaA"
40 | },
41 | {
42 | "id": "file_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRGh1dw",
43 | "created": null,
44 | "modified": "2020-05-12T13:08:11Z",
45 | "size": 16296,
46 | "path": "/this-is-a-very-looooooooooooooooooooooooooooooooooong-file.png",
47 | "ancestors": [
48 | {
49 | "type": "folder",
50 | "id": "root",
51 | "name": "Dropbox",
52 | "id_type": "path"
53 | }
54 | ],
55 | "parent": {
56 | "type": "folder",
57 | "id": "root",
58 | "name": "Dropbox",
59 | "id_type": "path"
60 | },
61 | "account": 1234567890,
62 | "ids": {
63 | "default": "file_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRGh1dw",
64 | "path": "file_L3Rlc3QucG5n"
65 | },
66 | "id_type": "default",
67 | "api": "storage",
68 | "type": "file",
69 | "name": "this-is-a-very-looooooooooooooooooooooooooooooooooong-file.png",
70 | "mime_type": "image/png",
71 | "downloadable": true,
72 | "href": "https://api.kloudless.com/v1/accounts/1234567890/storage/files/file_aWQ6NmU3bXRCZUg0c0FBQUFBQUFBRGh1dw",
73 | "raw_id": "id:6e7mtBeH4sAAAAAAAADhuw"
74 | }
75 | ],
76 | "type": "object_list",
77 | "api": "storage"
78 | }
--------------------------------------------------------------------------------
/src/loader/js/vue/creators.js:
--------------------------------------------------------------------------------
1 | import filePicker from '../interface';
2 | import { ChooserButton, SaverButton } from './DefaultButtons';
3 |
4 | const create = filePickerType => (customComponent) => {
5 | const isChooser = filePickerType === 'chooser';
6 | const wrappedComponent = customComponent || (
7 | isChooser ? ChooserButton : SaverButton);
8 | const wrappedCompName = wrappedComponent.name || 'component';
9 | return {
10 | name: isChooser
11 | ? `createChooser-${wrappedCompName}`
12 | : `createSaver-${wrappedCompName}`,
13 | props: {
14 | ...wrappedComponent.props,
15 | options: {
16 | type: Object,
17 | required: true,
18 | },
19 | },
20 | data() {
21 | return {
22 | picker: null,
23 | };
24 | },
25 | methods: {
26 | choose(...args) {
27 | this.$emit('click', ...args);
28 | this.picker.choose();
29 | },
30 | save(...args) {
31 | this.$emit('click', ...args);
32 | this.picker.save();
33 | },
34 | initPicker() {
35 | // deep clone options
36 | const options = JSON.parse(JSON.stringify(this.options));
37 | if (this.options.oauth) {
38 | options.oauth = this.options.oauth;
39 | }
40 | this.picker = filePicker.picker(options);
41 | this.picker.on('raw', (...args) => {
42 | this.$emit(...args);
43 | });
44 | },
45 | },
46 | watch: {
47 | options: {
48 | handler() {
49 | this.picker.destroy();
50 | this.initPicker();
51 | },
52 | deep: true,
53 | },
54 | },
55 | render(createElement) {
56 | const { options, ...restProps } = this.$props;
57 | const element = createElement(wrappedComponent, {
58 | props: restProps,
59 | attrs: this.$attrs,
60 | // Listen for native click event if the wrapped component doesn't
61 | // explicitly emit it.
62 | nativeOn: {
63 | click: isChooser ? this.choose : this.save,
64 | },
65 | // Bind listeners to the wrapped component
66 | on: {
67 | ...this.$listeners,
68 | click: isChooser ? this.choose : this.save,
69 | },
70 | });
71 | return element;
72 | },
73 | mounted() {
74 | this.initPicker();
75 | },
76 | destroyed() {
77 | this.picker.destroy();
78 | },
79 | };
80 | };
81 |
82 | export const createChooser = create('chooser');
83 | export const createSaver = create('saver');
84 |
85 | export default {
86 | createChooser,
87 | createSaver,
88 | };
89 |
--------------------------------------------------------------------------------
/dev-server/static/translations-suite-sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "fr": {
3 | "global": {
4 | "select": "Sélectionnez",
5 | "cancel": "Annuler",
6 | "save": "Enregistrer",
7 | "upload": "Télécharger"
8 | },
9 | "files": {
10 | "name": "Nom",
11 | "size": "Taille de",
12 | "updated": "mis à Jour",
13 | "noFilesFound": "Aucun fichier Trouvé",
14 | "untitledFolder": "Dossier sans titre",
15 | "search": "Rechercher"
16 | },
17 | "accounts": {
18 | "confirmRemove": "Si vous supprimez ce compte, vous ne pourrez plus l'explorer. Êtes-vous sûr?",
19 | "connectedAccounts": "comptes connectés",
20 | "logout": "déconnexion",
21 | "manage": "Gérer vos comptes de stockage en nuage ici.",
22 | "chooseAccount": "Bienvenue! Veuillez choisir un service à connecter",
23 | "connectMore": "se Connecter plus!",
24 | "upload": "Télécharger",
25 | "uploadFromComputer": "Upload à partir de votre ordinateur"
26 | },
27 | "selector": {
28 | "myComputer": "Mon Ordinateur",
29 | "accounts": "les Comptes de"
30 | },
31 | "dropzone": {
32 | "message": "Glissez et déposez les fichiers ici, ou cliquez pour ouvrir le sélecteur de fichiers"
33 | },
34 | "computer": {
35 | "noSupport": "votre navigateur n'a pas de support Flash, Silverlight ou HTML5."
36 | },
37 | "addConfirm": {
38 | "confirm": "Confirmer la connexion au compte",
39 | "clickBelow": "cliquez ci-dessous pour connecter votre compte {serviceName} ",
40 | "connectAccount": "Connect"
41 | }
42 | },
43 | "zh-CN": {
44 | "global": {
45 | "select": "选择",
46 | "cancel": "取消",
47 | "save": "保存",
48 | "upload": "上传"
49 | },
50 | "files": {
51 | "name": "名称",
52 | "size": "大小",
53 | "updated": "更新自",
54 | "noFilesFound": "找不到文件",
55 | "untitledFolder": "未命名",
56 | "search": "查找"
57 | },
58 | "accounts": {
59 | "confirmRemove": "若您移除此帐号, 您将无法浏览档案, 是否确定?",
60 | "connectedAccounts": "已连接的帐号",
61 | "logout": "登出",
62 | "manage": "管理您的云帐号.",
63 | "chooseAccount": "欢迎使用! 请选择您要连接的服务",
64 | "connectMore": "连接更多服务!",
65 | "upload": "上传",
66 | "uploadFromComputer": "从您的电脑上传"
67 | },
68 | "selector": {
69 | "myComputer": "我的电脑",
70 | "accounts": "帐号"
71 | },
72 | "dropzone": {
73 | "message": "拖放档案至此或点击以开启File Picker"
74 | },
75 | "computer": {
76 | "noSupport": "您的浏览器不支援Flash, Silverlight 或 HTML5."
77 | },
78 | "addConfirm": {
79 | "confirm": "确认连接帐号",
80 | "clickBelow": "点选下方以连接您的 {serviceName} 帐号",
81 | "connectAccount": "连接"
82 | }
83 | }
84 | }
--------------------------------------------------------------------------------
/config/picker-plugins.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Return a list of webpack plugins used to build picker script and page.
3 | */
4 |
5 | const path = require('path');
6 | const webpack = require('webpack');
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
9 |
10 | const srcPath = path.resolve(__dirname, '../src');
11 | const cldrPath = path.resolve(__dirname, '../bower_components/cldr-data/');
12 |
13 | const getLocalizationCopyData = (pickerDistPath) => {
14 | const localeDistPath = path.join(pickerDistPath, 'localization');
15 | const cldrDistPath = path.join(localeDistPath, 'cldr-data');
16 | const copyData = [
17 | {
18 | from: path.resolve(srcPath, 'picker/localization'),
19 | to: localeDistPath,
20 | },
21 | {
22 | context: cldrPath,
23 | from: 'main/**/@(numbers|ca-gregorian|timeZoneNames).json',
24 | to: cldrDistPath,
25 | },
26 | {
27 | context: cldrPath,
28 | from: 'supplemental/@(likelySubtags|numberingSystems|timeData|weekData)'
29 | + '.json',
30 | to: cldrDistPath,
31 | },
32 | ];
33 |
34 | if (process.env.BUILD_LICENSE === 'AGPL') {
35 | copyData.push({
36 | from: path.resolve(__dirname,
37 | '../node_modules/@kloudless/file-picker-plupload-module/i18n/'),
38 | to: path.join(localeDistPath, 'plupload/i18n/'),
39 | });
40 | }
41 | return copyData;
42 | };
43 |
44 | module.exports = function getPickerPlugins(pickerDistPath) {
45 | return [
46 | new webpack.ProvidePlugin({
47 | // expose jquery to global for jquery plugins
48 | $: 'jquery',
49 | jQuery: 'jquery',
50 | 'window.jQuery': 'jquery',
51 | // expose mOxie to global for plupload
52 | mOxie: ['@kloudless/file-picker-plupload-module/moxie', 'mOxie'],
53 | }),
54 | // copy less.js
55 | new CopyWebpackPlugin([
56 | {
57 | from: path.resolve(srcPath, '../node_modules/less/dist/less.min.js'),
58 | to: path.join(pickerDistPath, 'less.js'),
59 | },
60 | ]),
61 | // copy *.less
62 | new CopyWebpackPlugin([
63 | {
64 | from: path.resolve(srcPath, 'picker/css/'),
65 | to: path.join(pickerDistPath, 'less/'),
66 | },
67 | ]),
68 | // copy localization and cldr data
69 | new CopyWebpackPlugin(getLocalizationCopyData(pickerDistPath)),
70 | /** Attach an id to the picker script tag
71 | * for util.getBaseUrl() to identify the script tag
72 | * This plugin must be put after all HtmlWebpackPlugins
73 | */
74 | new ScriptExtHtmlWebpackPlugin({
75 | custom: {
76 | test: /picker\.js$/,
77 | attribute: 'id',
78 | value: 'kloudless-file-picker-script',
79 | },
80 | }),
81 | ];
82 | };
83 |
--------------------------------------------------------------------------------
/storybook-test/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Common constants either used in storybook or jest.
3 | *
4 | * Due to storybook's configuration, only env variables with 'STORYBOOK_' prefix
5 | * are available in storybook. To provide the same interface for both jest and
6 | * storybook. We modified .storybook/main.js to add the prefix for those
7 | * storybook specific env vars.
8 | * As a result, both *.story.js and *.test.js files can import this file to get
9 | * constants defined from env vars.
10 | *
11 | * The effective path:
12 | * storybook: env vars -> .storybook/main.js -> config.js -> *.story.js
13 | * jest: env vars -> config.js -> *.test.js
14 | * Therefore, env vars with STORYBOOK_ are only available in storybook while
15 | * those without STORYBOOK_ are only available for jest.
16 | */
17 | const { devServerPorts } = require('../config/common');
18 |
19 | /**
20 | * Constants for both storybook and jest.
21 | */
22 |
23 | const PICKER_URL = (
24 | process.env.STORYBOOK_PICKER_URL || process.env.PICKER_URL
25 | || `http://localhost:${devServerPorts.picker}/file-picker/v2/index.html`);
26 | const BASE_URL = (
27 | process.env.STORYBOOK_BASE_URL || process.env.BASE_URL
28 | || 'https://api.kloudless.com');
29 | const KLOUDLESS_ACCOUNT_TOKEN = (
30 | process.env.STORYBOOK_KLOUDLESS_ACCOUNT_TOKEN
31 | || process.env.KLOUDLESS_ACCOUNT_TOKEN || '');
32 | const KLOUDLESS_APP_ID = (
33 | process.env.STORYBOOK_KLOUDLESS_APP_ID || process.env.KLOUDLESS_APP_ID || '');
34 |
35 |
36 | const DEFAULT_GLOBAL_OPTIONS = {
37 | baseUrl: BASE_URL,
38 | pickerUrl: PICKER_URL,
39 | };
40 |
41 | const DEFAULT_CHOOSER_OPTIONS = {
42 | app_id: KLOUDLESS_APP_ID,
43 | tokens: [KLOUDLESS_ACCOUNT_TOKEN],
44 | multiselect: true,
45 | link: false,
46 | computer: true,
47 | services: ['all'],
48 | types: ['all'],
49 | display_backdrop: true,
50 | };
51 |
52 | const DEFAULT_SAVER_OPTIONS = {
53 | ...DEFAULT_CHOOSER_OPTIONS,
54 | files: [
55 | {
56 | url: 'https://s3-us-west-2.amazonaws.com/static-assets.kloudless.com/'
57 | + 'static/kloudless-logo-white.png',
58 | name: 'kloudless-logo.png',
59 | },
60 | ],
61 | };
62 |
63 |
64 | /**
65 | * Constants for jest.
66 | */
67 |
68 | // id is determined by the story name.
69 | const STORY_URL = {
70 | chooser: 'http://localhost:9001/iframe.html?id=e2e-test--chooser',
71 | saver: 'http://localhost:9001/iframe.html?id=e2e-test--saver',
72 | dropzone: 'http://localhost:9001/iframe.html?id=e2e-test--dropzone',
73 | };
74 |
75 | // Filename to store test data.
76 | const TEST_DATA = '.fp_e2e_test_data.json';
77 |
78 | module.exports = {
79 | PICKER_URL,
80 | BASE_URL,
81 | KLOUDLESS_ACCOUNT_TOKEN,
82 | KLOUDLESS_APP_ID,
83 | STORY_URL,
84 | DEFAULT_GLOBAL_OPTIONS,
85 | DEFAULT_CHOOSER_OPTIONS,
86 | DEFAULT_SAVER_OPTIONS,
87 | TEST_DATA,
88 | };
89 |
--------------------------------------------------------------------------------
/src/picker/css/icons.less:
--------------------------------------------------------------------------------
1 | .icon__base(@url) {
2 | width: 100%;
3 | height: 100%;
4 | background: url(@url) no-repeat;
5 | background-position: center;
6 | background-size: contain;
7 | }
8 |
9 | /**
10 | * Icons that have different URL on hover.
11 | */
12 | .icon__hoverable-base(@url, @hovered_url) {
13 | cursor: pointer;
14 | .icon__base(@url);
15 |
16 | &:hover {
17 | .icon__base(@hovered_url);
18 | }
19 |
20 | // Won't show on the UI but makes browsers to load the image first.
21 | &::after {
22 | background: url(@hovered_url);
23 | content: '';
24 | }
25 | }
26 |
27 | .icon {
28 | display: flex;
29 | align-items: center;
30 | width: 20px;
31 | height: 20px;
32 | }
33 |
34 | .icon__service {
35 | width: 100%;
36 | height: auto;
37 | }
38 |
39 | .icon__computer {
40 | .icon__base(@icon_my_device);
41 | }
42 |
43 | .icon__accounts {
44 | .icon__base(@icon_manage_accounts);
45 | }
46 |
47 | .icon__checked {
48 | .icon__base(@icon_checked);
49 | }
50 |
51 | .icon__root-dir {
52 | .icon__base(@icon_root_dir);
53 | }
54 |
55 | .icon__folder {
56 | .icon__base(@icon_folder);
57 | }
58 |
59 | .icon__file {
60 | .icon__base(@icon_file);
61 | }
62 |
63 | .icon__back {
64 | .icon__hoverable-base(@icon_back, @icon_back_hovered);
65 | }
66 |
67 | .icon__next {
68 | .icon__base(@icon_next);
69 | }
70 |
71 | .icon__logout {
72 | .icon__hoverable-base(@icon_logout, @icon_logout_hovered);
73 | }
74 |
75 | .icon__close {
76 | .icon__hoverable-base(@icon_close, @icon_close_hovered);
77 | }
78 |
79 | .icon__new-folder {
80 | .icon__base(@icon_new_folder);
81 | }
82 |
83 | .icon__refresh {
84 | .icon__base(@icon_refresh);
85 | }
86 |
87 | .icon__search {
88 | .icon__hoverable-base(@icon_search, @icon_search_hovered);
89 | }
90 |
91 | .icon__brand {
92 | .icon__base(@icon_brand);
93 | }
94 |
95 | .icon__sort {
96 | .icon__base(@icon_sort);
97 | }
98 |
99 | .icon__sort--desc {
100 | .icon__base(@icon_sort_desc);
101 | }
102 |
103 | .icon__sort--asc {
104 | .icon__base(@icon_sort_asc);
105 | }
106 |
107 | .icon--button {
108 | cursor: pointer;
109 | border-radius: @border_radius;
110 |
111 | &:hover {
112 | background-color: @icon_btn_hover_color;
113 | }
114 | }
115 |
116 | .icon--xsmall {
117 | width: 12px;
118 | height: 12px;
119 | }
120 |
121 | .icon--small {
122 | width: 16px;
123 | height: 16px;
124 | }
125 |
126 | .icon--large {
127 | width: 24px;
128 | height: 24px;
129 | }
130 |
131 | .icon__loading {
132 | .icon__base(@icon_loading);
133 | }
134 |
135 | .icon--disabled {
136 | .icon__search {
137 | .icon__base(@icon_search);
138 |
139 | &:hover {
140 | cursor: unset;
141 | .icon__base(@icon_search);
142 | }
143 | }
144 | }
145 |
146 | .icon--xlarge {
147 | width: 60px;
148 | height: 60px;
149 |
150 | .icon__computer {
151 | .icon__base(@icon_my_device_lg);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/dev-server/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIJKQIBAAKCAgEAyYpJrK2pCTr9bGD5S1l3SuxXjMK6J20Q7DzzHGhk16to6pDc
3 | AmhhuZ5sE6eHTgcMfwWdyEv+A4tJFTobu5GzOv6oxZoe4K94CiTcNow8DBeSansj
4 | wCvt6Jy2hnE8zcYQ4OL8OKLkN3lBebyvV2iD90Y8okUrHiHYNqbGt1MGf32ZSX+L
5 | vXKNmG9NLZJHeS+7XQm/kNNNUu0dhG6hxYvIs39EkN7edP0n04sp689K+8wdOByd
6 | S78HElc3wkd1k+99nET6fYm0kDqLvyDxjWAI11mKcWutUXvKAWHO/T9EgOTjSl2y
7 | F99+QL29ocE9Pd5xf7g5FzzpQ38aFDsEBI0Q0Pr+zRGVfHhcZgwn3q+PQt2lTZ8e
8 | rN6IXam0u7Q54dipvq6bpbLo6INhN64RUVLcXV8PObZm6je5AuKRx6ENm+5xcS+a
9 | w9tv0veHI6gwW+NJ6b/t0CjZE83Yct4I2gypP/9X1qdjZdWt9Dd1CeRfaMb7Pi/9
10 | dfi09+7goug6Jf+0NpCc6ni6b3+gMKKock39S23aetuCYWMan49SOkS33riJiwEr
11 | F3AoBU7lWtwXI713YJihWUXW0GxS1nkfz55+8p4ODdEVciEH+gxrXMAU4lEI9JnZ
12 | eh54Pwd2Vr3ElPbrnberNdPJmhcn1meL5D/pr4DTnAacdPq+6JnbIa4ZIGECAwEA
13 | AQKCAgEAnEnXFEefW/8Y42DYMexxK/LXedrQDR7xDqG9TXxPJ0hPlgc79coIbq9B
14 | 1IQH0yR4NlCeqOL1Wr46sHFdbDlyqf8t1f7MjyTDi+pFsy9QqXfmHRjdAnuOhOJy
15 | UROjOVetYxfiy/DV1Yb8lXES8E+mdq1K+/NzmmxYhRkT5LpHwboekvaL4R/iWiGF
16 | 0h8rufif7WhP2/lgbzxdtYMthEoAG+JHw6hxDnv61HuIyMr5tQX2arsV59V9oGVu
17 | YgSUU2JXIEYFkjCli2s7T742U5HziTxwtb/wqc27OxMkNxNHBkV5VQQPxZ+VApOh
18 | aLy3xzCnB4pPC4w6wBLioGmblgPm0yg58CZ3tbKZKKRdx3bD0d9FjYfjCymjU96B
19 | O2u+8tRun2EOfHS9+Hf54g3uUkWKayE/1k4g1kXKLDPe0BRx+9Nh6iMjHi7FAjkA
20 | ZvYfhMpnIRpgom5mcVhToSnh/ttvwTMG57BKkNFG7Qh/NvlmmbOR9bnKk1uL09IL
21 | l0LnQNXVEtYffWXhhGxGiK+KX+m2rDXzwKVhIUVHCK6TcnT6aR3rHC4cUD1UFy/P
22 | c5/9hLXIqrlBTiFKuQRvQfPZmSZ1nREY8Ecio/1dlk+vFB4Z2NPKezs7rzW+HYS7
23 | D/QYeICqsR9JMt9rets/qg1YwGRpLZy0/3bCME6WIWKSzPKDFgECggEBAOpnUknY
24 | hpTG04+9sc+j4555hnzi/0zRu7VdhTdQHGziSCBwTBtoaDD3sBreRoO9+OfA+F9Q
25 | du7ufkQWYEhUYJ3wgaZBe1ZRGbG4GfzllLjF2rYrP4WvVbPG3E63txhU6KEu+wif
26 | aZ42NXL5Lp7v8Tz1v8cE6ZiMcaXuOjwKC/daAb+md8WmV6kfufz+VePwjIglGph/
27 | Pbr84WvccVAhrge2VBCR+7r4YXlnk46WpZafH78NAUioaRVUx3kz2cxk31B7ciez
28 | DMzMiFI6zsbtyYnizWKOSHvQiZVOhlKFaJPYeCvLmseq/wbIB6puqWPQ+f0hjD2n
29 | iCaZCyLEeGRfp7ECggEBANwb2E8HE61o93lUchpid9fH02/20Hk1xhT/CtIXFTQA
30 | BYhTrh6lx8ox3lYi/iLdERosW3FtKyYNaXVVSuZ4QCdCra9iHzcy4zt/352zCHqJ
31 | ezL5wihkbVd8veSFKr6pAM4I+AiyRNVlHbivevUDiNssGjOTPqQfyH6Q0maU2MaB
32 | tvN3PcWcWPhELG3GzjoAi8bZ0nWOeYdVJun8MS7KxJS3ZhTvEjWc0s+vbYr4Uu+p
33 | Eoxo2xBO5u0Mg2x3H2W6Wd7VyYKAsaHgp9ST2UDNR28nIBCLBd12cE+wKtnNBIHD
34 | 8WbJvlMfOpuzRGmVXkFYnusOOzTvh6sAf9coJx7T37ECggEAGK66joYbXc0199vc
35 | vmWekVBwpfPtODSZlHZ9kZ6A78JAIBJTIUu0NPvP8nRXboXxkM3UGY1KiMxaRWcp
36 | ylQAPIFX1Z7tkuBFWV5udh/isjY7WpVhQf19g8m75xoXUJuYR2jADF9k6sEAjdPJ
37 | YfkYKPgjspxE0MhxKyzTuwC/09MJfhnUYN6sOmXZ2tcZSkBJPAjULRyw7mC0h/wV
38 | fn+daLh9T6VfoYeIFBWhBxG77AljeWWwLet17UYZHx0joQ86KKpnEeEbxvD+pdIF
39 | dhMR9tAIGomq/kauRieXo1bi9TaFKO9uo/nQkvE7RWoTsiwONuZycweSIaZZ8tZH
40 | ayJbcQKCAQAU3drq2wvlg676ZKQQlkcwQLRtx+NJqmxl7yvScPfEnz/nBa/bHfJL
41 | 2+BtIBIhsf1+Erh0j1no2Jqn0fcw1DOYxTx8BPxamktqh5vmcmOaYlA4q+7ZL3Qi
42 | OCQ0dt9vhcwavETvZh8ab+SabqBke/pMdOji/NGSc6TpQsd/jBrk7sUuXZ6Qjlrs
43 | 5mGj4pYIb6bQqjPGi0RALTVsN0leW0C5rI1T7Lo4NO7TW5kx9IrAR7IHd6VU7XX9
44 | AROwg7aJSVpdwrzAz3yHkSm4AHA3MX7VLctfZh6fOSw6kcPuM/56Yt4O1Y9ih/Jh
45 | cmRI8i0mWsVuGUJJqW+eKYT1G8xhospxAoIBAQDD/lsj2geerI8yEjfeNrH2Y9OB
46 | Bf7zsrwPIZRt5GOIgSCzm1T7dp73Ke0xfYoOhJsYqZIrTSyKQ6qIPmLU7BatfD7U
47 | zjoTHTXLt9dZIS/UJd9jLeeHokv9QByQsBIBWs/fLmOhgnos1kOfcVsxr2nflXG7
48 | DIMLz0IeLwXO6i5s+vUV3A4608kFHHSKujCz2BdoM+TOirvL3GiIoG/ijkZAVtyt
49 | vOM6+jrYuU2X7W+KW1zXZgDJZTL/doJzQWCWwaSIH9JwDT+yEk9SuEfMNABupqBF
50 | 2TU6x60DVtyILs5xKVzUP7c7J/8kqbr+v6Yy7OowYZKK/4XmtZ39v3G4tyx1
51 | -----END RSA PRIVATE KEY-----
52 |
--------------------------------------------------------------------------------
/src/picker/js/izitoast-helper.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: ["error", { "args": "none" }] */
2 | import iziToast from 'izitoast';
3 | import localization from './localization';
4 | import util from './util';
5 | import { ERROR_MSG_TIMEOUT } from './constants';
6 |
7 | const supportsCopy =
8 | (typeof document.queryCommandSupported === 'function') &&
9 | (typeof document.execCommand === 'function') &&
10 | document.queryCommandSupported('Copy');
11 |
12 | /**
13 | * Show an error toast.
14 | * If there is detail message, a copy button is present and timeout is disabled.
15 | * @param {string} message
16 | * @param {object=} options
17 | * @param {string} options.detail - Detail message.
18 | */
19 | function error(message, options = {}) {
20 | if (message === undefined || message === null || message === '') {
21 | return;
22 | }
23 | const { detail } = options;
24 |
25 | const buttons = [];
26 | let msg = message;
27 | if (detail) {
28 | if (util.isIE) {
29 | // IE11 doesn't support . https://caniuse.com/#feat=details
30 | msg = `${msg}
${detail}`;
31 | } else {
32 | msg = `
33 | ${msg}
34 |
35 | show details
36 | ${detail}
37 | `;
38 | }
39 | if (supportsCopy) {
40 | buttons.push([
41 | ``,
44 | (instance, toast) => {
45 | const eltemp = document.createElement('textarea');
46 | eltemp.value = `${message} (detail: ${detail})`;
47 | eltemp.readOnly = true;
48 | eltemp.className = 'visual-invisible';
49 | eltemp.setAttribute('aria-hidden', 'true');
50 | document.body.appendChild(eltemp);
51 | eltemp.select();
52 | document.execCommand('copy');
53 | document.body.removeChild(eltemp);
54 | }, false,
55 | ]);
56 | }
57 | }
58 |
59 | iziToast.show({
60 | buttons,
61 | progressBar: !detail,
62 | timeout: detail ? false : ERROR_MSG_TIMEOUT,
63 | layout: detail ? 2 : 1, // 1: small layout, 2: medium layout
64 | pauseOnHover: true,
65 | drag: false, // to allow user to select text
66 | position: 'bottomCenter',
67 | title: 'Error',
68 | message: msg,
69 | theme: 'custom-error',
70 | icon: 'material-icons',
71 | iconText: 'highlight_off',
72 | });
73 | }
74 |
75 | function success(message) {
76 | iziToast.show({
77 | close: false,
78 | drag: false, // to allow user to select text
79 | timeout: false,
80 | position: 'center',
81 | title: 'Success',
82 | message,
83 | progressBar: false,
84 | layout: 1,
85 | overlay: true,
86 | icon: 'material-icons',
87 | iconText: 'check_circle',
88 | theme: 'custom-success',
89 | displayMode: 'replace',
90 | buttons: [[
91 | ``,
94 | (instance, toast) => { instance.hide({}, toast); },
95 | false,
96 | ]],
97 | });
98 | }
99 |
100 | /**
101 | * Clear all toasts.
102 | */
103 | function destroy() {
104 | iziToast.destroy();
105 | }
106 |
107 | export default { error, destroy, success };
108 |
--------------------------------------------------------------------------------
/config/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const CssNano = require('cssnano');
4 | const AutoPrefixer = require('autoprefixer');
5 |
6 | const common = require('./common');
7 |
8 | const root = path.resolve(__dirname, '../');
9 | const isDevelopment = process.env.NODE_ENV === 'development';
10 |
11 | /**
12 | * @param {string} fileType less or css
13 | * @param {string} viewType picker or loader
14 | */
15 | function getStyleLoaders(fileType, viewType) {
16 | const result = [];
17 | const miniCssExtractLoader = {
18 | loader: MiniCssExtractPlugin.loader,
19 | options: { hmr: isDevelopment },
20 | };
21 | const postCssLoader = {
22 | loader: 'postcss-loader',
23 | options: {
24 | plugins: isDevelopment ? [AutoPrefixer()] : [AutoPrefixer(), CssNano()],
25 | },
26 | };
27 | if (viewType === 'picker') {
28 | result.push(miniCssExtractLoader);
29 | } else if (viewType === 'loader') {
30 | result.push('style-loader');
31 | }
32 | result.push('css-loader', postCssLoader);
33 | if (fileType === 'less') {
34 | result.push('less-loader');
35 | }
36 | return result;
37 | }
38 |
39 | function getRules() {
40 | const rules = [
41 | {
42 | test: /\.css$/,
43 | use: getStyleLoaders('css', 'picker'),
44 | },
45 | {
46 | test: /loader\/css\/.*\.less$/,
47 | use: getStyleLoaders('less', 'loader'),
48 | },
49 | {
50 | test: /picker\/css\/.*\.less$/,
51 | use: getStyleLoaders('less', 'picker'),
52 | },
53 | {
54 | test: /\.jsx?$/,
55 | use: 'babel-loader',
56 | },
57 | {
58 | test: /\.pug$/,
59 | use: {
60 | loader: 'pug-loader',
61 | options: {
62 | pretty: isDevelopment,
63 | },
64 | },
65 | },
66 | {
67 | test: /\.png$/,
68 | use: {
69 | loader: 'url-loader',
70 | options: {
71 | limit: 10000,
72 | },
73 | },
74 | },
75 | {
76 | test: /\.woff2?$/,
77 | loader: 'url-loader',
78 | options: {
79 | limit: 75 * 1024,
80 | // Enable a CommonJS module syntax
81 | // eslint-disable-next-line max-len
82 | // REF: https://stackoverflow.com/questions/59070216/webpack-file-loader-outputs-object-module
83 | esModule: false,
84 | outputPath: 'font',
85 | },
86 | },
87 | ];
88 |
89 | if (process.env.BUILD_LICENSE !== 'AGPL') {
90 | rules.push(
91 | {
92 | // eslint-disable-next-line max-len
93 | test: /(@kloudless\/file-picker-plupload-module\/*)|(plupload-helper\.js)/,
94 | use: 'null-loader',
95 | },
96 | );
97 | }
98 | return rules;
99 | }
100 |
101 | module.exports = {
102 | context: root,
103 | resolve: {
104 | extensions: ['.js', '.jsx'],
105 | modules: common.resolvePaths,
106 | alias: {
107 | // set these cldr alias to avoid webpack build error
108 | cldr$: 'cldrjs',
109 | cldr: 'cldrjs/dist/cldr',
110 | },
111 | },
112 | module: {
113 | rules: getRules(),
114 | },
115 | plugins: [
116 | new MiniCssExtractPlugin({
117 | filename: '[name].css',
118 | }),
119 | ],
120 | performance: {
121 | maxEntrypointSize: 10 * 1024 * 1024,
122 | maxAssetSize: 10 * 1024 * 1024,
123 | },
124 | };
125 |
--------------------------------------------------------------------------------
/storybook-react/stories/index.jsx:
--------------------------------------------------------------------------------
1 |
2 | /* eslint-disable react/prop-types */
3 | import React from 'react';
4 | import { storiesOf } from '@storybook/react';
5 | import { withKnobs } from '@storybook/addon-knobs';
6 | import { configureReadme } from 'storybook-readme';
7 | import reactReadme from '../../README.react.md';
8 | import footerReadme from '../../storybook-common/README.footer.md';
9 |
10 | import {
11 | Dropzone, Chooser, Saver, createChooser, createSaver,
12 | } from '../../src/loader/js/react';
13 | import { genProps, GreenButton } from './helpers';
14 |
15 | const stories = storiesOf('File Picker with React', module);
16 |
17 | const GreenSaver = createSaver(GreenButton);
18 | const GreenChooser = createChooser(GreenButton);
19 |
20 | configureReadme({
21 | DocPreview: ({ children }) => (
22 | {children}
23 | ),
24 | StoryPreview: ({ children }) => (
25 | {children}
26 | ),
27 | FooterPreview: ({ children }) => (
28 | {children}
29 | ),
30 | footer: footerReadme,
31 | });
32 |
33 | stories.addDecorator(withKnobs);
34 | stories.addParameters({ readme: { content: reactReadme } });
35 |
36 | stories.add('Chooser/Saver/createSaver/createChooser', () => (
37 |
38 |
39 |
40 | You can play with the buttons below and see how it works in the
41 | right panel:
42 | -
43 | Knobs
44 | edit props (ex: config.scope, config.client_id)
45 |
46 | -
47 | Actions
48 | print logs and arguments of callback functions
49 |
50 | -
51 | *
52 | We use bootstrap CSS here for demonstration
53 |
54 |
55 |
56 |
57 |
Chooser Example
58 |
59 |
60 |
61 |
62 |
63 |
createChooser Example
64 |
65 |
66 |
67 |
68 |
69 |
Saver Example
70 |
71 |
72 |
73 |
74 |
75 |
createSaver Example
76 |
77 |
78 |
79 |
80 |
81 | ));
82 |
83 | stories.add('Dropzone', () => (
84 |
85 |
86 |
87 | You can play with the buttons below and see how it works in right panel:
88 | -
89 | Knobs
90 | edit props (ex: config.scope, config.client_id)
91 |
92 | -
93 | Actions
94 | print logs and arguments of callback functions
95 |
96 | -
97 | *
98 | We use bootstrap CSS here for demonstration
99 |
100 |
101 |
102 |
103 |
Dropzone Example
104 |
105 |
106 |
107 |
108 |
109 | ));
110 |
--------------------------------------------------------------------------------
/storybook-test/tests/image/core/PuppeteerHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Based on BasePuppeteerHelper, do the following stuff to make sure the
3 | * snapshots are consistent.
4 | * 1. Disable CSS animations and transitions.
5 | * 2. Mock response data.
6 | * 3. Make sure font and image resources are loaded before taking snapshots.
7 | */
8 | import BasePuppeteerHelper from '../../integration/core/PuppeteerHelper';
9 | import ListFolderContentResponse from './folder_content_response.json';
10 | import GetAccountResponse from './get_account_response.json';
11 |
12 | const STYLE_DISABLE_ANIMATION = `
13 | *,
14 | *::after,
15 | *::before {
16 | transition-delay: 0s !important;
17 | transition-duration: 0s !important;
18 | animation-delay: -0.0001s !important;
19 | animation-duration: 0s !important;
20 | animation-play-state: paused !important;
21 | caret-color: transparent !important;
22 | color-adjust: exact !important;
23 | -webkit-animation-play-state: paused;
24 | -moz-animation-play-state: paused;
25 | }`;
26 |
27 | class PuppeteerHelper extends BasePuppeteerHelper {
28 | async init(...args) {
29 | // Call super's init();
30 | await super.init(...args);
31 |
32 | this.addRequestInterceptor(
33 | { url: '/storage/folders/root/contents' },
34 | { body: ListFolderContentResponse },
35 | { global: true },
36 | );
37 | this.addRequestInterceptor(
38 | { url: '/accounts/me/?active=True' },
39 | { body: GetAccountResponse },
40 | { global: true },
41 | );
42 | // Disable loader's animation.
43 | await this.loaderFrame.addStyleTag({ content: STYLE_DISABLE_ANIMATION });
44 | }
45 |
46 | async launch(...args) {
47 | await super.launch(...args);
48 | // Disable picker's animations.
49 | await this.viewFrame.addStyleTag({ content: STYLE_DISABLE_ANIMATION });
50 | }
51 |
52 | _waitForImage(options = {}) {
53 | const { timeout = 3000 } = options;
54 | // Wait for images of
to be loaded
55 | const waitForImageTag = this.viewFrame.waitForFunction(
56 | () => Array.from(document.images).every(i => i.complete),
57 | { timeout },
58 | );
59 | // Wait for background images to be loaded
60 | const waitForBgImage = this.viewFrame.waitForFunction(() => {
61 | const regexp = /url\("([^"]+)"\)/;
62 | const els = document.querySelectorAll('.icon > div');
63 | const loadBackgroundImgs = [];
64 | els.forEach((el) => {
65 | const { background } = window.getComputedStyle(el);
66 | const matches = background.match(regexp);
67 | if (matches !== null) {
68 | loadBackgroundImgs.push(new Promise((resolve) => {
69 | const img = new Image();
70 | img.onload = resolve;
71 | // eslint-disable-next-line prefer-destructuring
72 | img.src = matches[1];
73 | }));
74 | }
75 | });
76 | return Promise.all(loadBackgroundImgs);
77 | }, { timeout });
78 | return Promise.all([waitForImageTag, waitForBgImage]);
79 | }
80 |
81 | _waitForFont(options = {}) {
82 | const { timeout = 3000 } = options;
83 | // wait for fonts ready
84 | // https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts
85 | return this.page.evaluateHandle('document.fonts.ready', { timeout });
86 | }
87 |
88 | async assertScreenshot() {
89 | await this._waitForFont();
90 | await this._waitForImage();
91 | const image = await this.page.screenshot();
92 | expect(image).toMatchImageSnapshot();
93 | }
94 | }
95 |
96 | export default PuppeteerHelper;
97 |
--------------------------------------------------------------------------------
/src/loader/css/modal.less:
--------------------------------------------------------------------------------
1 | @import '../../picker/css/constants.less';
2 | @import '../../picker/css/variables.less';
3 |
4 | .body-kloudless-modal-backdrop-active {
5 | overflow: hidden;
6 | }
7 |
8 | .kloudless-modal {
9 | position: fixed;
10 | top: 50%;
11 | left: 50%;
12 | display: block;
13 | box-sizing: border-box;
14 | margin-top: -250px;
15 | margin-left: -320px;
16 | width: 640px;
17 | height: 0;
18 | border: 0;
19 | border-radius: @border_radius;
20 | opacity: 0;
21 | z-index: 999;
22 | box-shadow: 0 1px 8px 1px @primary_btn_shadow_color;
23 | transition: opacity 0.2s, height 0.2s step-end;
24 | }
25 |
26 | .kloudless-modal--backdrop {
27 | box-shadow: 0 1px 8px 1px #4b4b4b;
28 | }
29 |
30 | .kloudless-modal--attached {
31 | position: relative;
32 | top: 0;
33 | left: 0;
34 | margin-top: 0;
35 | margin-left: 0;
36 | width: 100%;
37 | z-index: auto;
38 | }
39 |
40 | .kloudless-modal--opened {
41 | height: 515px;
42 | opacity: 1;
43 | transition: opacity 0.2s, height 0.2s step-start;
44 | }
45 |
46 | // Make the view take the full screen in modal mode on mobile screen
47 | .kloudless-modal--opened:not(
48 | [class*="kloudless-modal--attached"]) {
49 | @media only screen and (max-width: @MODAL_WIDTH),
50 | only screen and (max-device-width: @MODAL_WIDTH) {
51 | position: fixed;
52 | left: 0;
53 | margin-left: 0;
54 | width: 100%;
55 |
56 | html,
57 | body {
58 | overflow: hidden;
59 | }
60 | }
61 |
62 | @media only screen and (max-width: @BREAKING_POINT),
63 | only screen and (max-device-width: @BREAKING_POINT) {
64 | position: fixed;
65 | top: 0;
66 | left: 0;
67 | margin: 0;
68 | width: 100%;
69 | height: 100%;
70 |
71 | html,
72 | body {
73 | overflow: hidden;
74 | }
75 | }
76 | }
77 |
78 | .kloudless-modal--dropzone {
79 | /**
80 | * The drop picker is always opened. But set its iframe opacity to almost 0
81 | * before users dropping files or after uploading process succeed/canceled.
82 | */
83 | height: 100%;
84 | // Cannot set opacity to 0 because that makes the iframe not clickable in
85 | // Chrome
86 | opacity: 0.01;
87 | // Place it on top of the Dropzone text so that drag and drop works on
88 | // the entire element
89 | z-index: 2;
90 | transition: none;
91 | }
92 |
93 | .kloudless-modal--dropzone-dropped {
94 | opacity: 1;
95 | }
96 |
97 | .backdrop-div {
98 | position: fixed;
99 | top: 0;
100 | left: 0;
101 | display: none;
102 | margin: 0;
103 | padding: 0;
104 | width: 100%;
105 | height: 100%;
106 | background-color: black;
107 | opacity: 0.6;
108 | z-index: 998;
109 | }
110 |
111 | .backdrop-div.backdrop-div--active {
112 | display: block;
113 | }
114 |
115 | .kloudless-dropzone-container {
116 | cursor: pointer;
117 | position: relative;
118 | box-sizing: border-box;
119 | width: 600px;
120 | height: 100px;
121 | color: rgb(71, 71, 71);
122 | background-color: rgb(245, 246, 247);
123 | border-style: dashed;
124 | border-width: 1px;
125 | border-radius: @border_radius;
126 | box-shadow: 0 1px 8px 1px @primary_btn_shadow_color;
127 |
128 | > span {
129 | position: absolute;
130 | top: 0;
131 | bottom: 0;
132 | right: 0;
133 | left: 0;
134 | margin: auto;
135 | width: 100%;
136 | height: 14px;
137 | font-size: 14px;
138 | text-align: center;
139 | }
140 | }
141 |
142 | .kloudless-dropzone-container--dropped {
143 | min-width: 700px;
144 | min-height: 515px;
145 | border-style: none;
146 | }
147 |
--------------------------------------------------------------------------------
/storybook-vue/stories/index.js:
--------------------------------------------------------------------------------
1 | import { storiesOf } from '@storybook/vue';
2 | import { withKnobs } from '@storybook/addon-knobs';
3 | import { configureReadme } from 'storybook-readme';
4 | import vueReadme from '../../README.vue.md';
5 | import footerReadme from '../../storybook-common/README.footer.md';
6 |
7 | import {
8 | createChooser, createSaver, Chooser, Saver, Dropzone,
9 | } from '../../src/loader/js/vue';
10 | import {
11 | CustomButton,
12 | HintComponent,
13 | createExampleElement,
14 | genProps,
15 | } from './helper';
16 |
17 | /**
18 | * constants
19 | */
20 | const APP_ID = (
21 | process.env.STORYBOOK_KLOUDLESS_APP_ID
22 | || 'Ydwe2dlIONXa66YL7Hm0yZwdykjvwQRihlTDdwuvUQ_Il_Sx');
23 |
24 | const OPTIONS = {
25 | app_id: APP_ID,
26 | };
27 |
28 | const FILES = [{
29 | // eslint-disable-next-line max-len
30 | url: 'https://s3-us-west-2.amazonaws.com/static-assets.kloudless.com/logo_mark.png',
31 | name: 'kloudless-logo.png',
32 | }];
33 |
34 | const COMPONENTS = [
35 | {
36 | name: 'Chooser',
37 | component: Chooser,
38 | props: {
39 | options: { ...OPTIONS },
40 | title: 'test chooser',
41 | },
42 | attrs: {
43 | class: 'btn btn-outline-dark',
44 | disabled: false,
45 | },
46 | },
47 | {
48 | name: 'createChooser',
49 | component: createChooser(CustomButton),
50 | props: {
51 | options: { ...OPTIONS },
52 | },
53 | attrs: {
54 | class: 'btn btn-success',
55 | disabled: false,
56 | },
57 | },
58 | {
59 | name: 'Saver',
60 | component: Saver,
61 | props: {
62 | options: { ...OPTIONS, files: FILES },
63 | title: 'test saver',
64 | },
65 | attrs: {
66 | class: 'btn btn-outline-dark',
67 | disabled: false,
68 | },
69 | },
70 | {
71 | name: 'createSaver',
72 | component: createSaver(CustomButton),
73 | props: {
74 | options: { ...OPTIONS, files: FILES },
75 | },
76 | attrs: {
77 | class: 'btn btn-success',
78 | disabled: false,
79 | },
80 | },
81 | ];
82 |
83 | const stories = storiesOf('Kloudless File Picker with Vue', module);
84 |
85 | /**
86 | * Configure storybook
87 | */
88 | configureReadme({ footer: footerReadme });
89 | stories.addDecorator(withKnobs);
90 | stories.addParameters({
91 | readme: {
92 | content: vueReadme,
93 | DocPreview: {
94 | template: '
',
95 | },
96 | StoryPreview: {
97 | template: '
',
98 | },
99 | FooterPreview: {
100 | template: '
',
101 | },
102 | },
103 | });
104 |
105 | stories.add('Chooser/Saver/createSaver/createChooser', () => ({
106 | props: genProps(COMPONENTS),
107 | render(createElement) {
108 | return createElement(
109 | 'div',
110 | [
111 | createElement(HintComponent),
112 | ...COMPONENTS.map(createExampleElement.bind(this, createElement)),
113 | ],
114 | );
115 | },
116 | }));
117 |
118 | const COMPONENTS2 = [{
119 | name: 'Dropzone',
120 | component: Dropzone,
121 | props: {
122 | options: { ...OPTIONS },
123 | },
124 | attrs: {},
125 | }];
126 |
127 | stories.add('Dropzone', () => ({
128 | props: genProps(COMPONENTS2),
129 | render(createElement) {
130 | return createElement(
131 | 'div',
132 | [
133 | createElement(HintComponent),
134 | ...COMPONENTS2.map(createExampleElement.bind(this, createElement)),
135 | ],
136 | );
137 | },
138 | }));
139 |
--------------------------------------------------------------------------------
/src/picker/js/router-helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import sammy from 'sammy';
3 | import logger from 'loglevel';
4 | import storage from './storage';
5 | import config from './config';
6 | import { VIEW } from './constants';
7 |
8 | function init(picker) {
9 | const router = sammy(function () { // eslint-disable-line func-names
10 | // Do not use arrow function in order to use `this`
11 |
12 | // Override setLocation to disable history modifications.
13 | this.disable_push_state = true;
14 | this.setLocation = (path) => {
15 | this.runRoute('get', path);
16 | };
17 |
18 | /*
19 | * Routes
20 | */
21 |
22 | // Switch to the accounts page.
23 | this.get('#/accounts', () => {
24 | logger.debug('Accounts page requested.');
25 | picker.switchViewTo(VIEW.accounts);
26 | });
27 |
28 | // Reconnect an erroneously disconnected account.
29 | // WARNING: THIS HAS NOT YET BEEN IMPLEMENTED.
30 | // eslint-disable-next-line func-names
31 | this.get('#/account/reconnect/:id', function () {
32 | // Do not use arrow function in order to use `this`
33 | logger.debug(`Account reconnection invoked for id: ${this.params.id}.`);
34 | });
35 |
36 | // Disconnect an account.
37 | // eslint-disable-next-line func-names
38 | this.get('#/account/disconnect/:id', function () {
39 | // Do not use arrow function in order to use `this`
40 | logger.debug(`Account disconnection invoked for id: ${this.params.id}.`);
41 | picker.manager.deleteAccount(
42 | this.params.id,
43 | picker.view_model.delete_accounts_on_logout(),
44 | (account_data) => {
45 | // post message for account
46 | picker.view_model.postMessage('deleteAccount',
47 | account_data.account);
48 | // store accounts
49 | storage.storeAccounts(config.app_id, picker.manager.accounts());
50 | },
51 | );
52 | });
53 |
54 | // Switch to the files page.
55 | this.get('#/files', () => {
56 | logger.debug('File view requested.');
57 | picker.switchViewTo(VIEW.files);
58 | });
59 | // Switch to the files view of a particular account.
60 | // eslint-disable-next-line func-names
61 | this.get('#/files/:account', function () {
62 | // Do not use arrow function in order to use `this`
63 | logger.debug(`Switching to files of account: ${this.params.account}.`);
64 | picker.switchViewTo(VIEW.files);
65 | picker.manager.active(picker.manager.getByAccount(this.params.account));
66 | });
67 |
68 | this.get('#/search', () => {
69 | logger.debug('Switching to search files');
70 | picker.switchViewTo(VIEW.search);
71 | });
72 |
73 | // Switch to the computer view
74 | this.get('#/computer', () => {
75 | logger.debug('Switching to computer view');
76 | picker.switchViewTo(VIEW.computer);
77 | });
78 | // Switch to the dropzone view
79 | this.get('#/dropzone', () => {
80 | picker.switchViewTo(VIEW.dropzone);
81 | });
82 | // Confirm add account button
83 | this.get('#/addConfirm', () => {
84 | picker.switchViewTo(VIEW.addConfirm);
85 | });
86 |
87 | /*
88 | * Additional initialization steps.
89 | */
90 |
91 | this.get('#/', () => {
92 | this.setLocation('#/accounts');
93 | });
94 |
95 | this.get(/.*/, () => {
96 | // Pass.
97 | // Add this to avoid 404 error from sammy router.
98 | });
99 | });
100 | return router;
101 | }
102 |
103 | export default { init };
104 |
--------------------------------------------------------------------------------
/storybook-vue/stories/helper.js:
--------------------------------------------------------------------------------
1 | import { action } from '@storybook/addon-actions';
2 | import { text, object, boolean } from '@storybook/addon-knobs';
3 |
4 | const EVENTS = [
5 | 'success',
6 | 'cancel',
7 | 'error',
8 | 'open',
9 | 'close',
10 | 'selected',
11 | 'addAccount',
12 | 'deleteAccount',
13 | 'startFileUpload',
14 | 'finishFileUpload',
15 | 'logout',
16 | 'click',
17 | 'drop',
18 | ];
19 |
20 | /**
21 | * generate event handlers that managed by storybook-action
22 | */
23 | function genEventHandlers(name) {
24 | return EVENTS.reduce((result, event) => {
25 | result[event] = action(`(${name}) received '${event}' event`);
26 | return result;
27 | }, {});
28 | }
29 |
30 | export const CustomButton = {
31 | name: 'custom-button',
32 | template: `
33 | `,
38 | };
39 |
40 | export const HintComponent = {
41 | template: `
42 |
43 | You can play with the buttons below and see how it works in the right
44 | panel:
45 | -
46 | Knobs
47 | edit props (ex: options.app_id, title)
48 |
49 | -
50 | Actions
51 | print logs and arguments of event listeners
52 |
53 | -
54 | *
55 | We use bootstrap CSS here for demonstration
56 |
57 |
`,
58 | };
59 |
60 | const ExampleWrapperComponent = {
61 | props: ['name'],
62 | template: `
63 |
64 |
{{ name }} Example
65 |
66 |
67 |
68 |
69 | `,
70 | };
71 |
72 | /**
73 | * create element in following mockup:
74 | *
75 | *
76 | *
77 | */
78 | export function createExampleElement(createElement, component) {
79 | const getKnobProps = obj => Object.keys(obj).reduce((result, key) => {
80 | result[key] = this.$props[`${component.name}${key}`];
81 | return result;
82 | }, {});
83 | return createElement(
84 | ExampleWrapperComponent,
85 | { props: { name: component.name } },
86 | [
87 | createElement(
88 | component.component,
89 | {
90 | props: getKnobProps(component.props),
91 | attrs: getKnobProps(component.attrs),
92 | on: genEventHandlers(component.name),
93 | },
94 | ),
95 | ],
96 | );
97 | }
98 |
99 |
100 | function genKnobProp(compName, propName, value) {
101 | const typeMap = {
102 | string: [String, text],
103 | object: [Object, object],
104 | boolean: [Boolean, boolean],
105 | };
106 | const type = typeof value;
107 | if (type in typeMap) {
108 | const [dataType, knobType] = typeMap[type];
109 | return {
110 | type: dataType,
111 | default: knobType(`(${compName}) ${propName}`, value, compName),
112 | };
113 | }
114 | throw Error(`genProp() error: un-expected type '${typeof value}'`);
115 | }
116 |
117 | /**
118 | * generate Vue props that managed by storybook-knobs
119 | */
120 | export function genProps(components) {
121 | return components.reduce((result, comp) => {
122 | const { props, attrs, name: compName } = comp;
123 | Object.keys(props).forEach((prop) => {
124 | result[`${compName}${prop}`] = genKnobProp(compName, prop, props[prop]);
125 | });
126 | Object.keys(attrs).forEach((attr) => {
127 | result[`${compName}${attr}`] = genKnobProp(compName, attr, attrs[attr]);
128 | });
129 | return result;
130 | }, {});
131 | }
132 |
--------------------------------------------------------------------------------
/src/picker/templates/accounts.pug:
--------------------------------------------------------------------------------
1 | .container(data-bind="css: {'container--is-loading': !$root.initComplete()}")
2 | // ko ifnot: $root.initComplete
3 | .container__loading-wrapper
4 | .container__loading
5 | .container__loading-icon
6 | // /ko
7 | // ko if: $root.initComplete
8 | .container__header
9 | .flex-row.align-items-center
10 | .flex-no-shrink(data-bind="css: {'invisible': accounts.all().length === 0}")
11 | .icon.icon--large
12 | .icon__back(data-bind="click: $root.setLocation.bind($data, '#/files')")
13 | // ko if: accounts.all().length > 0
14 | .flex-grow(data-bind='translate: "accounts/manage"')
15 | // /ko
16 | // ko ifnot: accounts.all().length > 0
17 | .flex-grow(data-bind='translate: "accounts/chooseAccount"')
18 | // /ko
19 | .flex-no-shrink.icon.icon--large(
20 | data-bind="css: {'invisible': attachMode()}")
21 | .icon__close(
22 | data-bind='click: cancel, css: $root.e2eSelectors.J_CLOSE_BTN')
23 |
24 | .container__body.container__body--scroll
25 | // ko if: accounts.all().length > 0
26 | .box.mb(data-bind='with: accounts')
27 | .box__title(data-bind='translate: "accounts/connectedAccounts"')
28 | .box__section(
29 | data-bind='foreach: {data: Object.keys(ko.toJS(by_service)), as: "service"}')
30 | .list
31 | // ko if: $root.services()[service]
32 | // ko foreach: ko.toJS($parent.by_service)[service]
33 | .list__item(data-bind='click: $root.setLocation.bind($data, "#/files/" + account)')
34 | .icon
35 | img.icon__service(
36 | data-bind='attr: {src: $root.services()[service].logo}')
37 | .list__text(data-bind='text: account_name')
38 | // ko if: $root.enable_logout
39 | .icon(data-bind=`
40 | clickBubble: false,
41 | click: function (data, element) {
42 | if ($root.localizedConfirmPopup("accounts/confirmRemove")) {
43 | $root.setLocation("#/account/disconnect/" + account);
44 | }
45 | }`)
46 | .icon__logout
47 | // /ko
48 | // /ko
49 | // /ko
50 | // ko if: $index() === (Object.keys($parent.by_service()).length - 1) && $parent.computer()
51 | .list__item
52 | .icon
53 | .icon__computer
54 | .list__text(data-bind=`
55 | click: $root.setLocation.bind($data, "#/computer"),
56 | translate: "accounts/uploadFromComputer"`)
57 | // /ko
58 | // ko if: $root.enable_logout
59 | .box__divider
60 | .box__section
61 | .link-btn(data-bind=`
62 | click: function (){logout($root.delete_accounts_on_logout())},
63 | translate: "accounts/logout"`)
64 | // /ko
65 | // /ko
66 |
67 | .box.mb
68 | .box__title(data-bind="translate: 'accounts/connectMore', css: {'hidden': accounts.all().length === 0}")
69 | .box__section
70 | .service-list
71 | // ko foreach: {data: Object.values(services()), as: "service"}
72 | // ko ifnot: $parent.accounts.all().length > 0 && service.id == 'computer' || !service.visible
73 | .service-list__item(
74 | data-bind='click: $parent.accounts.connect.bind(null, id)')
75 | .service-list__img
76 | .icon.icon--xlarge
77 | // ko if: service.id === 'computer'
78 | .icon__computer
79 | // /ko
80 | // ko ifnot: service.id === 'computer'
81 | img.icon__service(data-bind='attr: {src: logo}')
82 | // /ko
83 | .service-list__text(data-bind='text: service.name')
84 | // /ko
85 | // /ko
86 | // /ko
87 | include footer
88 |
--------------------------------------------------------------------------------
/config/webpack.story.conf.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config to build loader, react-wrapper, vue-wrapper and picker
3 | * scripts and related assets.
4 | * Storybook server will include loader scripts in
5 | * storybook-test/.storybook/preview-head.html.
6 | */
7 | const webpack = require('webpack');
8 | const path = require('path');
9 | const HtmlWebpackPlugin = require('html-webpack-plugin');
10 | const baseWebpackConfig = require('./webpack.base.conf');
11 | const getPickerPlugins = require('./picker-plugins');
12 | const merge = require('./merge-strategy');
13 | const { devServerPorts } = require('./common');
14 |
15 | const BASE_CONFIG = merge(baseWebpackConfig, {
16 | mode: 'development',
17 | devtool: '#source-map',
18 | });
19 | const SRC_PATH = path.resolve(__dirname, '../src');
20 | const LOADER_BASE_URL = `http://localhost:${devServerPorts.loader}/`;
21 | const PICKER_BASE_URL = `http://localhost:${devServerPorts.picker}/`;
22 |
23 | module.exports = [
24 | // react-wrapper of loader
25 | merge(BASE_CONFIG, {
26 | name: 'react-loader',
27 | entry: {
28 | 'sdk/kloudless.picker.react': [
29 | 'webpack-hot-middleware/client?reload=false&quiet=false'
30 | + `&name=react-loader&path=${LOADER_BASE_URL}__webpack_hmr`,
31 | path.resolve(SRC_PATH, 'loader/js/react/index.js'),
32 | ],
33 | },
34 | output: {
35 | filename: '[name].js',
36 | libraryTarget: 'umd',
37 | library: 'filePickerReact',
38 | globalObject: 'window.Kloudless',
39 | // Tell WHR where to reload resources.
40 | publicPath: LOADER_BASE_URL,
41 | },
42 | plugins: [
43 | new webpack.HotModuleReplacementPlugin(),
44 | ],
45 | }),
46 | // vue-wrapper of loader
47 | merge(BASE_CONFIG, {
48 | name: 'vue-loader',
49 | entry: {
50 | 'sdk/kloudless.picker.vue': [
51 | 'webpack-hot-middleware/client?reload=false&quiet=false&name=vue-loader'
52 | + `&path=${LOADER_BASE_URL}__webpack_hmr`,
53 | path.resolve(SRC_PATH, 'loader/js/vue/index.js'),
54 | ],
55 | },
56 | output: {
57 | filename: '[name].js',
58 | libraryTarget: 'umd',
59 | library: 'filePickerVue',
60 | globalObject: 'window.Kloudless',
61 | // Tell WHR where to reload resources.
62 | publicPath: LOADER_BASE_URL,
63 | },
64 | plugins: [
65 | new webpack.HotModuleReplacementPlugin(),
66 | ],
67 | }),
68 | // loader
69 | merge(BASE_CONFIG, {
70 | mode: 'development',
71 | name: 'loader',
72 | entry: {
73 | 'sdk/kloudless.picker': [
74 | 'webpack-hot-middleware/client?reload=false&quiet=false&name=loader'
75 | + `&path=${LOADER_BASE_URL}__webpack_hmr`,
76 | path.resolve(__dirname, 'loader-export-helper.js'),
77 | ],
78 | },
79 | output: {
80 | filename: '[name].js',
81 | // Tell WHR where to reload resources.
82 | publicPath: LOADER_BASE_URL,
83 | },
84 | plugins: [
85 | new webpack.HotModuleReplacementPlugin(),
86 | ],
87 | }),
88 | // picker
89 | merge(BASE_CONFIG, {
90 | name: 'picker',
91 | entry: {
92 | 'file-picker/v2/picker': [
93 | 'webpack-hot-middleware/client?reload=true&quiet=false&name=picker'
94 | + `&path=${PICKER_BASE_URL}__webpack_hmr`,
95 | path.resolve(SRC_PATH, 'picker/js/app.js'),
96 | ],
97 | },
98 | output: {
99 | filename: '[name].js',
100 | // Tell WHR where to reload resources.
101 | publicPath: PICKER_BASE_URL,
102 | },
103 | plugins: [
104 | new webpack.HotModuleReplacementPlugin(),
105 | // picker page
106 | new HtmlWebpackPlugin({
107 | filename: 'file-picker/v2/index.html',
108 | template: path.resolve(SRC_PATH, 'picker/templates/index.pug'),
109 | chunks: ['file-picker/v2/picker'],
110 | }),
111 | ...getPickerPlugins('file-picker/v2/'),
112 | ],
113 | }),
114 | ];
115 |
--------------------------------------------------------------------------------
/src/picker/js/constants.js:
--------------------------------------------------------------------------------
1 | const FLAVOR = {
2 | saver: 'saver',
3 | chooser: 'chooser',
4 | dropzone: 'dropzone',
5 | };
6 |
7 | const VIEW = {
8 | accounts: 'accounts',
9 | files: 'files',
10 | search: 'search',
11 | computer: 'computer',
12 | dropzone: 'dropzone',
13 | addConfirm: 'addConfirm',
14 | };
15 |
16 | const TYPE_ALIAS = {
17 | all: ['files', 'folders'],
18 | text: [
19 | 'applescript', 'as', 'as3', 'c', 'cc', 'clisp', 'coffee', 'cpp', 'cs',
20 | 'css', 'csv', 'cxx', 'def', 'diff', 'erl', 'fountain', 'ft', 'h', 'hpp',
21 | 'htm', 'html', 'hxx', 'inc', 'ini', 'java', 'js', 'json', 'less', 'log',
22 | 'lua', 'm', 'markdown', 'mat', 'md', 'mdown', 'mkdn', 'mm', 'mustache',
23 | 'mxml', 'patch', 'php', 'phtml', 'pl', 'plist', 'properties', 'py', 'rb',
24 | 'sass', 'scss', 'sh', 'shtml', 'sql', 'tab', 'taskpaper', 'tex', 'text',
25 | 'tmpl', 'tsv', 'txt', 'url', 'vb', 'xhtml', 'xml', 'yaml', 'yml', '',
26 | ],
27 | documents: [
28 | 'csv', 'doc', 'dochtml', 'docm', 'docx', 'docxml', 'dot', 'dothtml',
29 | 'dotm', 'dotx', 'eps', 'fdf', 'key', 'keynote', 'kth', 'mpd', 'mpp', 'mpt',
30 | 'mpx', 'nmbtemplate', 'numbers', 'odc', 'odg', 'odp', 'ods', 'odt', 'pages',
31 | 'pdf', 'pdfxml', 'pot', 'pothtml', 'potm', 'potx', 'ppa', 'ppam', 'pps',
32 | 'ppsm', 'ppsx', 'ppt', 'ppthtml', 'pptm', 'pptx', 'pptxml', 'prn', 'ps',
33 | 'pwz', 'rtf', 'tab', 'template', 'tsv', 'txt', 'vdx', 'vsd', 'vss', 'vst',
34 | 'vsx', 'vtx', 'wbk', 'wiz', 'wpd', 'wps', 'xdf', 'xdp', 'xlam', 'xll',
35 | 'xlr', 'xls', 'xlsb', 'xlsm', 'xlsx', 'xltm', 'xltx', 'xps',
36 | ],
37 | images: [
38 | 'bmp', 'cr2', 'gif', 'ico', 'ithmb', 'jpeg', 'jpg', 'nef', 'png', 'raw',
39 | 'svg', 'tif', 'tiff', 'wbmp', 'webp',
40 | ],
41 | videos: [
42 | '3g2', '3gp', '3gpp', '3gpp2', 'asf', 'avi', 'dv', 'dvi', 'flv', 'm2t',
43 | 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'ogv', 'ogx', 'rm',
44 | 'rmvb', 'ts', 'vob', 'webm', 'wmv',
45 | ],
46 | audio: [
47 | 'aac', 'aif', 'aifc', 'aiff', 'au', 'flac', 'm4a', 'm4b', 'm4p', 'm4r',
48 | 'mid', 'mp3', 'oga', 'ogg', 'opus', 'ra', 'ram', 'spx', 'wav', 'wma',
49 | ],
50 | ebooks: [
51 | 'acsm', 'aeh', 'azw', 'cb7', 'cba', 'cbr', 'cbt', 'cbz', 'ceb', 'chm',
52 | 'epub', 'fb2', 'ibooks', 'kf8', 'lit', 'lrf', 'lrx', 'mobi', 'opf', 'oxps',
53 | 'pdf', 'pdg', 'prc', 'tebr', 'tr2', 'tr3', 'xeb', 'xps',
54 | ],
55 | };
56 |
57 | const MIME_TYPE_ALIAS = {
58 | text: [
59 | 'application/octet-stream',
60 | ],
61 | documents: [
62 | 'application/vnd.google-apps.spreadsheet',
63 | 'application/vnd.google-apps.document',
64 | 'application/vnd.google-apps.drawing',
65 | 'application/vnd.google-apps.presentation',
66 | ],
67 | images: [
68 | 'application/vnd.google-apps.drawing',
69 | 'application/vnd.google-apps.photo',
70 | ],
71 | videos: [
72 | 'application/vnd.google-apps.video',
73 | ],
74 | audio: [
75 | 'application/vnd.google-apps.audio',
76 | ],
77 | // The MIME type that mapped by `''`
78 | _unknown_: [
79 | 'application/octet-stream',
80 | ],
81 | };
82 |
83 | // Set default error message timeout as 10 seconds.
84 | const ERROR_MSG_TIMEOUT = 10000;
85 |
86 | // Selectors for end-to-end testing
87 | const E2E_SELECTORS = {
88 | J_ROW: 'j-row',
89 | J_CLOSE_BTN: 'j-close-btn',
90 | J_SELECT_BTN: 'j-select-btn',
91 | J_SAVE_BTN: 'j-save-btn',
92 |
93 | // TODO: add to iziToast-helper.js after DEV-3728 gets merged
94 | J_ERROR_DIALOG: 'j-error-dialog',
95 | J_SUCCESS_DIALOG: 'j-success-dialog',
96 | J_DIALOG_OK_BTN: 'j-dialog-ok-btn',
97 | };
98 |
99 | export {
100 | TYPE_ALIAS,
101 | MIME_TYPE_ALIAS,
102 | VIEW,
103 | FLAVOR,
104 | ERROR_MSG_TIMEOUT,
105 | E2E_SELECTORS,
106 | };
107 | export default {
108 | TYPE_ALIAS,
109 | MIME_TYPE_ALIAS,
110 | VIEW,
111 | FLAVOR,
112 | ERROR_MSG_TIMEOUT,
113 | E2E_SELECTORS,
114 | };
115 |
--------------------------------------------------------------------------------
/src/picker/js/accounts.js:
--------------------------------------------------------------------------------
1 | /* global $ */
2 | /* eslint-disable camelcase, func-names */
3 | import ko from 'knockout';
4 | import logger from 'loglevel';
5 | import Account from './models/account';
6 | import Authenticator from './auth';
7 | import config from './config';
8 |
9 | function AccountManager() {
10 | this.accounts = ko.observableArray([]);
11 | this.active = ko.observable({});
12 | }
13 |
14 | // Connect an account of a particular service, then fire callbacks on init.
15 | AccountManager.prototype.addAccount = function (
16 | service, oauthParams, callbacks,
17 | ) {
18 | logger.debug('Starting authentication.');
19 | const response = Authenticator.authenticate(
20 | service, oauthParams, (data) => {
21 | logger.debug('Authenticated for: ', data.service || data.scope);
22 | const created = new Account( // eslint-disable-line no-unused-vars
23 | { key: { scheme: 'Bearer', key: data.access_token } },
24 | callbacks.on_account_ready, callbacks.on_fs_ready,
25 | );
26 | },
27 | );
28 |
29 | if (response.authUsingIEXDFrame) {
30 | callbacks.on_confirm_with_iexd();
31 | }
32 | };
33 |
34 | /**
35 | * Add authed account
36 | * @param {object} authedAccount - account object
37 | */
38 | AccountManager.prototype.addAuthedAccount = function (authedAccount) {
39 | logger.debug('Add authed account');
40 |
41 | // Don't allow duplicate accounts
42 | this.accounts.remove(account => account.account === authedAccount.account);
43 |
44 | this.accounts.push(authedAccount);
45 | };
46 |
47 | // Remove an account by Account ID.
48 | AccountManager.prototype.removeAccount = function (account_id) {
49 | // eslint-disable-next-line eqeqeq
50 | this.accounts.remove(account => account.account == account_id);
51 | // Remove the account from this.active
52 | if (this.active().account === account_id) {
53 | if (this.accounts()[0] !== undefined) {
54 | logger.debug('Change the active account to ', this.accounts()[0]);
55 | this.active(this.accounts()[0]);
56 | } else {
57 | logger.debug('Change the active account to an empty object');
58 | this.active({});
59 | }
60 | }
61 | };
62 |
63 | // Send a DELETE request to server to delete the account, then call
64 | // removeAccount().
65 | AccountManager.prototype.deleteAccount = function deleteAccount(
66 | account_id, delete_on_server, on_success_callback,
67 | ) {
68 | let account_data = {};
69 | const accounts = this.accounts();
70 | for (let i = 0; i < accounts.length; i += 1) {
71 | if (accounts[i].account == account_id) { // eslint-disable-line eqeqeq
72 | account_data = accounts[i];
73 | break;
74 | }
75 | }
76 | if (Object.keys(account_data).length === 0) {
77 | logger.warn('Account failed to remove');
78 | alert('Error occurred. Please try again!'); // eslint-disable-line no-alert
79 | return;
80 | }
81 | if (delete_on_server) {
82 | let request = $.ajax({ // eslint-disable-line no-unused-vars
83 | url: config.getAccountUrl(),
84 | type: 'DELETE',
85 | headers: {
86 | Authorization: `${account_data.key.scheme} ${account_data.key.key}`,
87 | },
88 | }).done(() => {
89 | this.removeAccount(account_data.account);
90 | on_success_callback(account_data);
91 | }).fail(() => {
92 | logger.warn('Account failed to remove');
93 | // eslint-disable-next-line no-alert
94 | alert('Error occurred. Please try again!');
95 | }).always(() => {
96 | request = null;
97 | });
98 | } else {
99 | this.removeAccount(account_data.account);
100 | on_success_callback(account_data);
101 | }
102 | };
103 |
104 | // Retrieve an account by Account ID. Returns null if account not found.
105 | AccountManager.prototype.getByAccount = function (account_id) {
106 | // eslint-disable-next-line eqeqeq
107 | return ko.utils.arrayFirst(this.accounts(), a => a.account == account_id);
108 | };
109 |
110 | export default AccountManager;
111 |
--------------------------------------------------------------------------------
/src/picker/js/storage.js:
--------------------------------------------------------------------------------
1 | import logger from 'loglevel';
2 | import config from './config';
3 |
4 | const storage = {
5 | container: null,
6 | };
7 |
8 | try {
9 | // set the storage container
10 | if (config.persist === 'local' && window.localStorage) {
11 | storage.container = window.localStorage;
12 | } else if (config.persist === 'session' && window.sessionStorage) {
13 | storage.container = window.sessionStorage;
14 | } else {
15 | // "none" or window.localStorage/window.sessionStorage is falsy.
16 | storage.container = {};
17 | }
18 | } catch (err) {
19 | // Chrome and Edge will throw DOMException when accessing sessionStorage or
20 | // localStorage if 3rd party cookie disabled.
21 | // https://blog.zok.pw/web/2015/10/21/3rd-party-cookies-in-practice/
22 | logger.warn(
23 | 'Cannot access localStorage/sessionStorage. '
24 | + 'This might be due to third-party cookies being disabled.',
25 | );
26 | storage.container = {};
27 | }
28 |
29 | // Pass in accounts from an account manager
30 | storage.storeAccounts = function storeAccounts(appId, accounts) {
31 | if (!storage.container) {
32 | return;
33 | }
34 |
35 | const serviceNames = config.all_services().map(service => service.id);
36 |
37 | const key = `k-${appId}`;
38 | let appData = storage.container[key];
39 | try {
40 | appData = JSON.parse(appData);
41 | } catch (err) {
42 | appData = {};
43 | }
44 |
45 | // add accounts from the manager for currently visible services.
46 | const array = accounts.map(acc => JSON.stringify(acc));
47 |
48 | // add accounts already saved for currently invisible services.
49 | if (appData.accounts) {
50 | appData.accounts.forEach((acc) => {
51 | const account = JSON.parse(acc);
52 | if (!serviceNames.includes(account.service)) {
53 | array.push(acc);
54 | }
55 | });
56 | }
57 |
58 | // store the final array
59 | appData.accounts = array;
60 | storage.container[key] = JSON.stringify(appData);
61 | };
62 |
63 | // Return an array of accounts, initialize if necessary
64 | // the appData is stringified
65 | storage.loadAccounts = function loadAccounts(appId) {
66 | if (!storage.container) {
67 | return [];
68 | }
69 |
70 | const serviceNames = config.all_services().map(service => service.id);
71 | const key = `k-${appId}`;
72 | let appData = storage.container[key];
73 | if (!appData || appData.length === 0) {
74 | appData = {};
75 | appData.accounts = [];
76 | storage.container[key] = JSON.stringify(appData);
77 | return appData.accounts;
78 | }
79 |
80 | appData = JSON.parse(appData);
81 | // accounts also needs to be parsed
82 | const array = [];
83 | const { accounts } = appData;
84 | for (let i = 0; i < accounts.length; i += 1) {
85 | const acc = JSON.parse(accounts[i]);
86 | if (serviceNames.length === 0 || // Not loaded yet
87 | serviceNames.indexOf(acc.service) !== -1) {
88 | array.push(acc);
89 | }
90 | }
91 | return array;
92 | };
93 |
94 | storage.removeAllAccounts = function removeAllAccounts(appId) {
95 | if (!storage.container) {
96 | return;
97 | }
98 |
99 | const key = `k-${appId}`;
100 | let appData = storage.container[key];
101 | if (!appData || appData.length === 0) {
102 | appData = {};
103 | appData.accounts = [];
104 | storage.container[key] = JSON.stringify(appData);
105 | } else {
106 | appData = JSON.parse(appData);
107 | appData.accounts = [];
108 | storage.container[key] = JSON.stringify(appData);
109 | }
110 | };
111 |
112 | storage.storeId = function storeId(pickerId) {
113 | if (!storage.container) {
114 | return;
115 | }
116 | const key = 'k-explorerId';
117 | storage.container[key] = pickerId;
118 | };
119 |
120 | storage.loadId = function loadId() {
121 | if (!storage.container) {
122 | return null;
123 | }
124 | const key = 'k-explorerId';
125 | return storage.container[key];
126 | };
127 |
128 | export default storage;
129 |
--------------------------------------------------------------------------------