@mongoosejs/studio 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1864,10 +1864,10 @@ module.exports = app => app.component('models', {
1864
1864
 
1865
1865
  const axios = __webpack_require__(/*! axios */ "./node_modules/axios/dist/browser/axios.cjs");
1866
1866
  const client = axios.create({
1867
- baseURL: ''
1867
+ baseURL: 'http://localhost:8888/.netlify/functions'
1868
1868
  });
1869
1869
 
1870
- client.hasAPIKey = !!'';
1870
+ client.hasAPIKey = !!'http://localhost:8888/.netlify/functions';
1871
1871
 
1872
1872
  client.interceptors.request.use(req => {
1873
1873
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
@@ -1882,12 +1882,20 @@ exports.githubLogin = function githubLogin() {
1882
1882
  return client.post('/githubLogin', { state: window.location.href }).then(res => res.data);
1883
1883
  };
1884
1884
 
1885
+ exports.getWorkspaceTeam = function getWorkspaceTeam() {
1886
+ return client.post('/getWorkspaceTeam', { workspaceId: {"_id":"67a5366c745bb0e6735950dc","ownerId":"679ba73e4cc3ddc28f6ef6af","baseUrl":"https://web.zevo.io","members":[{"userId":"679ba73e4cc3ddc28f6ef6af","roles":["owner"]},{"userId":"67a91f3e5db099be8bd0f5c5","roles":["member"]}],"createdAt":"2025-02-06T22:23:40.903Z","updatedAt":"2025-02-09T21:35:14.905Z","__v":1,"name":"Zevo DEV","subscriptionTier":"pro"}._id }).then(res => res.data);
1887
+ };
1888
+
1889
+ exports.inviteToWorkspace = function inviteToWorkspace(params) {
1890
+ return client.post('/inviteToWorkspace', { workspaceId: {"_id":"67a5366c745bb0e6735950dc","ownerId":"679ba73e4cc3ddc28f6ef6af","baseUrl":"https://web.zevo.io","members":[{"userId":"679ba73e4cc3ddc28f6ef6af","roles":["owner"]},{"userId":"67a91f3e5db099be8bd0f5c5","roles":["member"]}],"createdAt":"2025-02-06T22:23:40.903Z","updatedAt":"2025-02-09T21:35:14.905Z","__v":1,"name":"Zevo DEV","subscriptionTier":"pro"}._id, ...params }).then(res => res.data);
1891
+ };
1892
+
1885
1893
  exports.github = function github(code) {
1886
- return client.post('/github', { code, workspaceId: {}._id }).then(res => res.data);
1894
+ return client.post('/github', { code, workspaceId: {"_id":"67a5366c745bb0e6735950dc","ownerId":"679ba73e4cc3ddc28f6ef6af","baseUrl":"https://web.zevo.io","members":[{"userId":"679ba73e4cc3ddc28f6ef6af","roles":["owner"]},{"userId":"67a91f3e5db099be8bd0f5c5","roles":["member"]}],"createdAt":"2025-02-06T22:23:40.903Z","updatedAt":"2025-02-09T21:35:14.905Z","__v":1,"name":"Zevo DEV","subscriptionTier":"pro"}._id }).then(res => res.data);
1887
1895
  };
1888
1896
 
1889
1897
  exports.me = function me() {
1890
- return client.post('/me', { workspaceId: {}._id }).then(res => res.data);
1898
+ return client.post('/me', { workspaceId: {"_id":"67a5366c745bb0e6735950dc","ownerId":"679ba73e4cc3ddc28f6ef6af","baseUrl":"https://web.zevo.io","members":[{"userId":"679ba73e4cc3ddc28f6ef6af","roles":["owner"]},{"userId":"67a91f3e5db099be8bd0f5c5","roles":["member"]}],"createdAt":"2025-02-06T22:23:40.903Z","updatedAt":"2025-02-09T21:35:14.905Z","__v":1,"name":"Zevo DEV","subscriptionTier":"pro"}._id }).then(res => res.data);
1891
1899
  };
1892
1900
 
1893
1901
  exports.hasAPIKey = client.hasAPIKey;
@@ -1914,7 +1922,7 @@ appendCSS(__webpack_require__(/*! ./navbar.css */ "./frontend/src/navbar/navbar.
1914
1922
 
1915
1923
  module.exports = app => app.component('navbar', {
1916
1924
  template: template,
1917
- props: ['user'],
1925
+ props: ['user', 'roles'],
1918
1926
  data: () => ({ nodeEnv: null, showFlyout: false }),
1919
1927
  computed: {
1920
1928
  routeName() {
@@ -1925,6 +1933,9 @@ module.exports = app => app.component('navbar', {
1925
1933
  },
1926
1934
  hasAPIKey() {
1927
1935
  return mothership.hasAPIKey;
1936
+ },
1937
+ canViewTeam() {
1938
+ return this.roles?.includes('owner') || this.roles?.includes('admin');
1928
1939
  }
1929
1940
  },
1930
1941
  async mounted() {
@@ -2002,9 +2013,15 @@ module.exports = [
2002
2013
  path: '/dashboard/:dashboardId',
2003
2014
  name: 'dashboard',
2004
2015
  component: 'dashboard'
2016
+ },
2017
+ {
2018
+ path: '/team',
2019
+ name: 'team',
2020
+ component: 'team'
2005
2021
  }
2006
2022
  ];
2007
2023
 
2024
+
2008
2025
  /***/ }),
2009
2026
 
2010
2027
  /***/ "./frontend/src/splash/splash.js":
@@ -2025,7 +2042,7 @@ module.exports = app => app.component('splash', {
2025
2042
  data: () => ({ error: null }),
2026
2043
  computed: {
2027
2044
  workspaceName() {
2028
- return {}.name;
2045
+ return {"_id":"67a5366c745bb0e6735950dc","ownerId":"679ba73e4cc3ddc28f6ef6af","baseUrl":"https://web.zevo.io","members":[{"userId":"679ba73e4cc3ddc28f6ef6af","roles":["owner"]},{"userId":"67a91f3e5db099be8bd0f5c5","roles":["member"]}],"createdAt":"2025-02-06T22:23:40.903Z","updatedAt":"2025-02-09T21:35:14.905Z","__v":1,"name":"Zevo DEV","subscriptionTier":"pro"}.name;
2029
2046
  }
2030
2047
  },
2031
2048
  async mounted() {
@@ -2050,6 +2067,74 @@ module.exports = app => app.component('splash', {
2050
2067
  });
2051
2068
 
2052
2069
 
2070
+ /***/ }),
2071
+
2072
+ /***/ "./frontend/src/team/new-invitation/new-invitation.js":
2073
+ /*!************************************************************!*\
2074
+ !*** ./frontend/src/team/new-invitation/new-invitation.js ***!
2075
+ \************************************************************/
2076
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2077
+
2078
+ "use strict";
2079
+
2080
+
2081
+ const mothership = __webpack_require__(/*! ../../mothership */ "./frontend/src/mothership.js");
2082
+ const template = __webpack_require__(/*! ./new-invitation.html */ "./frontend/src/team/new-invitation/new-invitation.html");
2083
+
2084
+ module.exports = app => app.component('new-invitation', {
2085
+ template,
2086
+ emits: ['close', 'invitationCreated'],
2087
+ data: () => ({
2088
+ githubUsername: '',
2089
+ email: '',
2090
+ role: null
2091
+ }),
2092
+ methods: {
2093
+ async inviteToWorkspace() {
2094
+ const { invitation } = await mothership.inviteToWorkspace({ githubUsername: this.githubUsername, email: this.email, roles: [this.role] });
2095
+ this.$emit('invitationCreated', { invitation });
2096
+ this.$emit('close');
2097
+ }
2098
+ }
2099
+ });
2100
+
2101
+
2102
+ /***/ }),
2103
+
2104
+ /***/ "./frontend/src/team/team.js":
2105
+ /*!***********************************!*\
2106
+ !*** ./frontend/src/team/team.js ***!
2107
+ \***********************************/
2108
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2109
+
2110
+ "use strict";
2111
+
2112
+
2113
+ const mothership = __webpack_require__(/*! ../mothership */ "./frontend/src/mothership.js");
2114
+ const template = __webpack_require__(/*! ./team.html */ "./frontend/src/team/team.html");
2115
+
2116
+ module.exports = app => app.component('team', {
2117
+ template,
2118
+ data: () => ({
2119
+ workspace: null,
2120
+ users: null,
2121
+ invitations: null,
2122
+ showNewInvitationModal: false
2123
+ }),
2124
+ async mounted() {
2125
+ const { workspace, users, invitations } = await mothership.getWorkspaceTeam();
2126
+ this.workspace = workspace;
2127
+ this.users = users;
2128
+ this.invitations = invitations;
2129
+ },
2130
+ methods: {
2131
+ getRolesForUser(user) {
2132
+ return this.workspace.members.find(member => member.userId === user._id)?.roles ?? [];
2133
+ }
2134
+ }
2135
+ });
2136
+
2137
+
2053
2138
  /***/ }),
2054
2139
 
2055
2140
  /***/ "./node_modules/mpath/index.js":
@@ -3143,7 +3228,7 @@ module.exports = "<transition name=\"modal\">\n <div class=\"modal-mask\">\n
3143
3228
  /***/ ((module) => {
3144
3229
 
3145
3230
  "use strict";
3146
- module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models button.gray {\n color: black;\n background-color: #eee;\n}\n\n.models .model-selector {\n background-color: #eee;\n flex-grow: 0;\n padding: 15px;\n padding-top: 0px;\n}\n\n.models h1 {\n margin-top: 0px;\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: scroll;\n max-height: calc(100vh - 56px);\n}\n\n.models .documents table {\n /* max-width: -moz-fit-content;\n max-width: fit-content; */\n width: 100%;\n table-layout: auto;\n font-size: small;\n padding: 0;\n margin-right: 1em;\n white-space: nowrap;\n z-index: -1;\n border-collapse: collapse;\n line-height: 1.5em;\n}\n\n.models .documents table th {\n position: sticky;\n top: 0px;\n background-color: white;\n z-index: 1;\n}\n\n.models .documents table th:after {\n content: \"\";\n position: absolute;\n left: 0;\n width: 100%;\n bottom: -1px;\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n}\n\n.models .documents table tr {\n color: black;\n border-spacing: 0px 0px;\n background-color: white;\n cursor: pointer;\n}\n\n.models .documents table tr:nth-child(even) {\n background-color: #f5f5f5;\n}\n\n.models .documents table tr:hover {\n background-color: #a7b9ff;\n}\n\n.models .documents table th,\ntd {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n padding: 0 16px;\n height: 48px;\n}\n\n.models textarea {\n font-size: 1.2em;\n}\n\n.models .path-type {\n color: rgba(0, 0, 0, 0.36);\n font-size: 0.8em;\n}\n\n.models .documents-menu {\n display: flex;\n margin: 0.25em;\n width: calc(100vw - 220px);\n}\n\n.models .documents-menu .search-input {\n flex-grow: 1;\n align-items: center;\n}\n\n.models .search-input input {\n padding: 0.25em 0.5em;\n font-size: 1.1em;\n border: 1px solid #ddd;\n border-radius: 3px;\n width: calc(100% - 1em);\n}\n\n.models .sort-arrow {\n padding-left: 10px;\n padding-right: 10px;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .documents .buttons {\n display: inline-flex;\n justify-content: space-around;\n align-items: center;\n}\n";
3231
+ module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models button.gray {\n color: black;\n background-color: #eee;\n}\n\n.models .model-selector {\n background-color: #eee;\n flex-grow: 0;\n padding: 15px;\n padding-top: 0px;\n}\n\n.models h1 {\n margin-top: 0px;\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: scroll;\n max-height: calc(100vh - 56px);\n}\n\n.models .documents table {\n /* max-width: -moz-fit-content;\n max-width: fit-content; */\n width: 100%;\n table-layout: auto;\n font-size: small;\n padding: 0;\n margin-right: 1em;\n white-space: nowrap;\n z-index: -1;\n border-collapse: collapse;\n line-height: 1.5em;\n}\n\n.models .documents table th {\n position: sticky;\n top: 42px;\n background-color: white;\n z-index: 1;\n}\n\n.models .documents table th:after {\n content: \"\";\n position: absolute;\n left: 0;\n width: 100%;\n bottom: -1px;\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n}\n\n.models .documents table tr {\n color: black;\n border-spacing: 0px 0px;\n background-color: white;\n cursor: pointer;\n}\n\n.models .documents table tr:nth-child(even) {\n background-color: #f5f5f5;\n}\n\n.models .documents table tr:hover {\n background-color: #a7b9ff;\n}\n\n.models .documents table th,\ntd {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n padding: 0 16px;\n height: 48px;\n}\n\n.models textarea {\n font-size: 1.2em;\n}\n\n.models .path-type {\n color: rgba(0, 0, 0, 0.36);\n font-size: 0.8em;\n}\n\n.models .documents-menu {\n position: fixed;\n background-color: white;\n z-index: 1;\n padding: 4px;\n display: flex;\n width: calc(100vw - 12rem);\n}\n\n.models .documents-menu .search-input {\n flex-grow: 1;\n align-items: center;\n}\n\n.models .search-input input {\n padding: 0.25em 0.5em;\n font-size: 1.1em;\n border: 1px solid #ddd;\n border-radius: 3px;\n width: calc(100% - 1em);\n}\n\n.models .sort-arrow {\n padding-left: 10px;\n padding-right: 10px;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .documents .buttons {\n display: inline-flex;\n justify-content: space-around;\n align-items: center;\n}\n";
3147
3232
 
3148
3233
  /***/ }),
3149
3234
 
@@ -3154,7 +3239,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
3154
3239
  /***/ ((module) => {
3155
3240
 
3156
3241
  "use strict";
3157
- module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)]\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div>\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <input class=\"w-full rounded-md p-1 outline-gray-300 text-lg\" type=\"text\" placeholder=\"Filter or text\" v-model=\"searchText\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"shouldShowFieldModal = true\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :filter=\"filter\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" style=\"margin-bottom: 0.5em\">\n <input type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <label :for=\"'path' + index\">{{path.path}}</label>\n </div>\n <div style=\"margin-top: 1em\">\n <button type=\"submit\" @click=\"filterDocuments()\" style=\"color: black;margin-right: 0.5em\">Filter Selection</button>\n <button type=\"submit\" @click=\"deselectAll()\" class=\"gray\" style=\"margin-right: 0.5em\">Deselect All</button>\n <button type=\"submit\" @click=\"resetDocuments()\" class=\"gray\">Cancel</button>\n\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n </div>\n</div>\n";
3242
+ module.exports = "<div class=\"models\">\n <div>\n <div class=\"flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48\">\n <div class=\"flex font-bold font-xl mt-4 pl-2\">\n Models\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </div>\n\n </div>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px]\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"flex-grow m-0\">\n <input class=\"w-full rounded-md p-1 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none\" type=\"text\" placeholder=\"Filter or text\" v-model=\"searchText\" />\n </form>\n <div>\n <span v-if=\"status === 'loading'\">Loading ...</span>\n <span v-if=\"status === 'loaded'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"shouldShowFieldModal = true\"\n type=\"button\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"outputType = 'table'\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"outputType = 'json'\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <td v-for=\"schemaPath in filteredPaths\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\n <div v-if=\"outputType === 'json'\">\n <div v-for=\"document in documents\" @click=\"$router.push('/model/' + currentModel + '/document/' + document._id)\" :key=\"document._id\">\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </div>\n </div>\n <div v-if=\"status === 'loading'\" class=\"loader\">\n <img src=\"images/loader.gif\">\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :filter=\"filter\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" style=\"margin-bottom: 0.5em\">\n <input type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <label :for=\"'path' + index\">{{path.path}}</label>\n </div>\n <div style=\"margin-top: 1em\">\n <button type=\"submit\" @click=\"filterDocuments()\" style=\"color: black;margin-right: 0.5em\">Filter Selection</button>\n <button type=\"submit\" @click=\"deselectAll()\" class=\"gray\" style=\"margin-right: 0.5em\">Deselect All</button>\n <button type=\"submit\" @click=\"resetDocuments()\" class=\"gray\">Cancel</button>\n\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n </div>\n</div>\n";
3158
3243
 
3159
3244
  /***/ }),
3160
3245
 
@@ -3176,7 +3261,7 @@ module.exports = ".navbar {\n width: 100%;\n background-color: #eee;\n}\n\n.ac
3176
3261
  /***/ ((module) => {
3177
3262
 
3178
3263
  "use strict";
3179
- module.exports = "<div class=\"navbar\">\n <div class=\"nav-left flex items-center gap-4 h-full\">\n <router-link to=\"/\">\n <img src=\"images/logo.svg\" alt=\"Mongoose Studio Logo\" />\n </router-link>\n <div v-if=\"!!nodeEnv\" class=\"inline-flex items-center rounded-md px-2 py-1 text-sm font-medium text-gray-900\" :class=\"warnEnv ? 'bg-red-300' : 'bg-yellow-300'\">\n {{nodeEnv}}\n </div>\n </div>\n <div class=\"nav-right h-full\">\n <div class=\"sm:ml-6 sm:flex sm:space-x-8 h-full\">\n <a\n href=\"#/\"\n class=\"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium\"\n :class=\"routeName === 'root' ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Documents</a>\n <a\n href=\"#/dashboards\"\n class=\"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium\"\n :class=\"routeName === 'dashboards' ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Dashboards</a>\n\n <div class=\"h-full flex items-center\" v-if=\"!user && hasAPIKey\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"h-full flex items-center relative\" v-clickOutside=\"hideFlyout\">\n <div>\n <button type=\"button\" @click=\"showFlyout = !showFlyout\" class=\"relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800\" id=\"user-menu-button\" aria-expanded=\"false\" aria-haspopup=\"true\">\n <span class=\"absolute -inset-1.5\"></span>\n <span class=\"sr-only\">Open user menu</span>\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n </button>\n </div>\n\n <div v-if=\"showFlyout\" class=\"absolute right-0 z-10 top-[82%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none\" role=\"menu\" aria-orientation=\"vertical\" aria-labelledby=\"user-menu-button\" tabindex=\"-1\">\n <!-- Active: \"bg-gray-100 outline-none\", Not Active: \"\" -->\n <span @click=\"logout\" class=\"cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Sign out</span>\n </div>\n </div>\n\n </div>\n </div>\n <div style=\"clear: both\"></div>\n</div>\n";
3264
+ module.exports = "<div class=\"navbar\">\n <div class=\"nav-left flex items-center gap-4 h-full\">\n <router-link to=\"/\">\n <img src=\"images/logo.svg\" alt=\"Mongoose Studio Logo\" />\n </router-link>\n <div v-if=\"!!nodeEnv\" class=\"inline-flex items-center rounded-md px-2 py-1 text-sm font-medium text-gray-900\" :class=\"warnEnv ? 'bg-red-300' : 'bg-yellow-300'\">\n {{nodeEnv}}\n </div>\n </div>\n <div class=\"nav-right h-full\">\n <div class=\"sm:ml-6 sm:flex sm:space-x-8 h-full\">\n <a\n href=\"#/\"\n class=\"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium\"\n :class=\"routeName === 'root' ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Documents</a>\n <a\n href=\"#/dashboards\"\n class=\"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium\"\n :class=\"routeName === 'dashboards' ? 'text-gray-900 border-ultramarine-500' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'\">Dashboards</a>\n\n <div class=\"h-full flex items-center\" v-if=\"!user && hasAPIKey\">\n <button\n type=\"button\"\n @click=\"loginWithGithub\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Login\n </button>\n </div>\n <div v-if=\"user && hasAPIKey\" class=\"h-full flex items-center relative\" v-clickOutside=\"hideFlyout\">\n <div>\n <button type=\"button\" @click=\"showFlyout = !showFlyout\" class=\"relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800\" id=\"user-menu-button\" aria-expanded=\"false\" aria-haspopup=\"true\">\n <span class=\"absolute -inset-1.5\"></span>\n <span class=\"sr-only\">Open user menu</span>\n <img class=\"size-8 rounded-full\" :src=\"user.picture\" alt=\"\">\n </button>\n </div>\n\n <div v-if=\"showFlyout\" class=\"absolute right-0 z-10 top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none\" role=\"menu\" aria-orientation=\"vertical\" aria-labelledby=\"user-menu-button\" tabindex=\"-1\">\n <router-link to=\"team\" v-if=\"canViewTeam\" @click=\"showFlyout = false\" class=\"cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Team</router-link>\n <span @click=\"logout\" class=\"cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200\" role=\"menuitem\" tabindex=\"-1\" id=\"user-menu-item-2\">Sign out</span>\n </div>\n </div>\n\n </div>\n </div>\n <div style=\"clear: both\"></div>\n</div>\n";
3180
3265
 
3181
3266
  /***/ }),
3182
3267
 
@@ -3191,6 +3276,28 @@ module.exports = "<div class=\"w-full h-full flex items-center justify-center\">
3191
3276
 
3192
3277
  /***/ }),
3193
3278
 
3279
+ /***/ "./frontend/src/team/new-invitation/new-invitation.html":
3280
+ /*!**************************************************************!*\
3281
+ !*** ./frontend/src/team/new-invitation/new-invitation.html ***!
3282
+ \**************************************************************/
3283
+ /***/ ((module) => {
3284
+
3285
+ "use strict";
3286
+ module.exports = "<div class=\"p-1\">\n <form class=\"space-y-4\">\n <div class=\"text-lg font-bold\">\n New Invitation\n </div>\n\n <div>\n <label for=\"githubUsername\" class=\"block text-sm/6 font-medium text-gray-900\">GitHub Username</label>\n <div class=\"mt-2\">\n <input type=\"githubUsername\" name=\"githubUsername\" id=\"githubUsername\" v-model=\"githubUsername\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"johnsmith12\">\n </div>\n </div>\n\n <div>\n <label for=\"email\" class=\"block text-sm/6 font-medium text-gray-900\">Email (Optional)</label>\n <div class=\"mt-2\">\n <input type=\"email\" name=\"email\" id=\"email\" v-model=\"email\" class=\"block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6\" placeholder=\"you@example.com\">\n </div>\n </div>\n\n <div>\n <label for=\"location\" class=\"block text-sm/6 font-medium text-gray-900\">Role</label>\n <div class=\"mt-2 grid grid-cols-1\">\n <select id=\"role\" name=\"role\" v-model=\"role\" class=\"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6\">\n <option value=\"admin\">Admin</option>\n <option value=\"member\">Member</option>\n <option value=\"readonly\">Read-only</option>\n </select>\n <svg class=\"pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\" data-slot=\"icon\">\n <path fill-rule=\"evenodd\" d=\"M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n </div>\n\n <async-button\n type=\"submit\"\n @click=\"inviteToWorkspace\"\n class=\"inline-flex justify-center rounded-md border border-transparent bg-forest-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-forest-green-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2\">\n Submit\n </async-button>\n </form>\n</div>\n";
3287
+
3288
+ /***/ }),
3289
+
3290
+ /***/ "./frontend/src/team/team.html":
3291
+ /*!*************************************!*\
3292
+ !*** ./frontend/src/team/team.html ***!
3293
+ \*************************************/
3294
+ /***/ ((module) => {
3295
+
3296
+ "use strict";
3297
+ module.exports = "<div class=\"mx-auto max-w-5xl py-6 px-2\">\n <div class=\"text-xl font-bold\">\n Current Members\n </div>\n <ul role=\"list\" class=\"divide-y divide-gray-100\">\n <li class=\"flex justify-between gap-x-6 py-5\" v-for=\"user in users\">\n <div class=\"flex min-w-0 gap-x-4\">\n <img class=\"size-12 flex-none rounded-full bg-gray-50\" :src=\"user.picture ?? 'images/logo.svg'\" alt=\"\">\n <div class=\"min-w-0 flex-auto\">\n <p class=\"text-sm/6 font-semibold text-gray-900\">{{user.name}}</p>\n <p class=\"mt-1 truncate text-xs/5 text-gray-500\">{{user.email}}</p>\n </div>\n </div>\n <div class=\"hidden shrink-0 sm:flex sm:flex-col sm:items-end\">\n <p class=\"text-sm/6 text-gray-900 capitalize\">{{getRolesForUser(user).join(', ')}}</p>\n <p class=\"mt-1 text-xs/5 text-gray-500\">Last seen <time datetime=\"2023-01-23T13:23Z\">3h ago</time></p>\n </div>\n </li>\n </ul>\n <div class=\"mt-6\">\n <div class=\"flex items-center justify-between\">\n <div class=\"text-xl font-bold\">\n Invitations\n </div>\n <div class=\"mt-4 sm:ml-16 sm:mt-0 sm:flex-none\">\n <button\n type=\"button\"\n @click=\"showNewInvitationModal = true\"\n class=\"block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n New Invitation\n </button>\n </div>\n </div>\n <div class=\"mt-8 flow-root\" v-if=\"invitations?.length > 0\">\n <div class=\"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8\">\n <div class=\"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8\">\n <table class=\"min-w-full divide-y divide-gray-300\">\n <thead>\n <tr>\n <th scope=\"col\" class=\"py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0\">GitHub Username</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Email</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Status</th>\n <th scope=\"col\" class=\"px-3 py-3.5 text-left text-sm font-semibold text-gray-900\">Role</th>\n </tr>\n </thead>\n <tbody class=\"divide-y divide-gray-200 bg-white\">\n <tr v-for=\"invitation in invitations\">\n <td class=\"whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0\">\n {{invitation.githubUsername}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.email}}\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n <span class=\"inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/20\">\n Pending\n </span>\n </td>\n <td class=\"whitespace-nowrap px-3 py-5 text-sm text-gray-500\">\n {{invitation.roles.join(', ')}}\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <div v-if=\"invitations?.length === 0\" class=\"mt-4\">\n <div class=\"text-center\">\n <svg class=\"mx-auto size-12 text-gray-400\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" aria-hidden=\"true\">\n <path vector-effect=\"non-scaling-stroke\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z\" />\n </svg>\n <h3 class=\"mt-2 text-sm font-semibold text-gray-900\">No invitations</h3>\n <p class=\"mt-1 text-sm text-gray-500\">You have no outstanding invitations</p>\n </div>\n </div>\n </div>\n\n <modal v-if=\"showNewInvitationModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"showNewInvitationModal = false\">&times;</div>\n <new-invitation @close=\"showNewInvitationModal = false\" @invitationCreated=\"invitations.push($event.invitation)\"></new-invitation>\n </template>\n </modal>\n</div>\n";
3298
+
3299
+ /***/ }),
3300
+
3194
3301
  /***/ "./node_modules/axios/dist/browser/axios.cjs":
3195
3302
  /*!***************************************************!*\
3196
3303
  !*** ./node_modules/axios/dist/browser/axios.cjs ***!
@@ -11049,6 +11156,8 @@ __webpack_require__(/*! ./modal/modal */ "./frontend/src/modal/modal.js")(app);
11049
11156
  __webpack_require__(/*! ./models/models */ "./frontend/src/models/models.js")(app);
11050
11157
  __webpack_require__(/*! ./navbar/navbar */ "./frontend/src/navbar/navbar.js")(app);
11051
11158
  __webpack_require__(/*! ./splash/splash */ "./frontend/src/splash/splash.js")(app);
11159
+ __webpack_require__(/*! ./team/team */ "./frontend/src/team/team.js")(app);
11160
+ __webpack_require__(/*! ./team/new-invitation/new-invitation */ "./frontend/src/team/new-invitation/new-invitation.js")(app);
11052
11161
 
11053
11162
  app.component('app-component', {
11054
11163
  template: `
@@ -11057,7 +11166,7 @@ app.component('app-component', {
11057
11166
  <splash />
11058
11167
  </div>
11059
11168
  <div v-else-if="!hasAPIKey || user">
11060
- <navbar :user="user" />
11169
+ <navbar :user="user" :roles="roles" />
11061
11170
  <div class="view">
11062
11171
  <router-view :key="$route.fullPath" />
11063
11172
  </div>
@@ -11083,14 +11192,17 @@ app.component('app-component', {
11083
11192
  if (mothership.hasAPIKey) {
11084
11193
  const token = window.localStorage.getItem('_mongooseStudioAccessToken');
11085
11194
  if (token) {
11086
- this.user = await mothership.me().then(res => res.user);
11195
+ const { user, roles } = await mothership.me();
11196
+ this.user = user;
11197
+ this.roles = roles;
11087
11198
  }
11088
11199
  }
11089
11200
  },
11090
11201
  setup() {
11091
11202
  const user = Vue.ref(null);
11203
+ const roles = Vue.ref(null);
11092
11204
 
11093
- const state = Vue.reactive({ user });
11205
+ const state = Vue.reactive({ user, roles });
11094
11206
  Vue.provide('state', state);
11095
11207
 
11096
11208
  return state;
@@ -590,6 +590,10 @@ video {
590
590
  border-width: 0;
591
591
  }
592
592
 
593
+ .pointer-events-none {
594
+ pointer-events: none;
595
+ }
596
+
593
597
  .fixed {
594
598
  position: fixed;
595
599
  }
@@ -614,8 +618,8 @@ video {
614
618
  right: 0px;
615
619
  }
616
620
 
617
- .top-\[82\%\] {
618
- top: 82%;
621
+ .top-\[90\%\] {
622
+ top: 90%;
619
623
  }
620
624
 
621
625
  .isolate {
@@ -626,6 +630,14 @@ video {
626
630
  z-index: 10;
627
631
  }
628
632
 
633
+ .col-start-1 {
634
+ grid-column-start: 1;
635
+ }
636
+
637
+ .row-start-1 {
638
+ grid-row-start: 1;
639
+ }
640
+
629
641
  .m-0 {
630
642
  margin: 0px;
631
643
  }
@@ -686,6 +698,10 @@ video {
686
698
  margin-right: 0.375rem;
687
699
  }
688
700
 
701
+ .mr-2 {
702
+ margin-right: 0.5rem;
703
+ }
704
+
689
705
  .mt-1 {
690
706
  margin-top: 0.25rem;
691
707
  }
@@ -738,6 +754,19 @@ video {
738
754
  display: flow-root;
739
755
  }
740
756
 
757
+ .grid {
758
+ display: grid;
759
+ }
760
+
761
+ .hidden {
762
+ display: none;
763
+ }
764
+
765
+ .size-12 {
766
+ width: 3rem;
767
+ height: 3rem;
768
+ }
769
+
741
770
  .size-5 {
742
771
  width: 1.25rem;
743
772
  height: 1.25rem;
@@ -768,6 +797,10 @@ video {
768
797
  height: 300px;
769
798
  }
770
799
 
800
+ .h-\[42px\] {
801
+ height: 42px;
802
+ }
803
+
771
804
  .h-\[calc\(100vh-55px\)\] {
772
805
  height: calc(100vh - 55px);
773
806
  }
@@ -800,6 +833,14 @@ video {
800
833
  width: 100%;
801
834
  }
802
835
 
836
+ .w-72 {
837
+ width: 18rem;
838
+ }
839
+
840
+ .min-w-0 {
841
+ min-width: 0px;
842
+ }
843
+
803
844
  .min-w-full {
804
845
  min-width: 100%;
805
846
  }
@@ -812,6 +853,14 @@ video {
812
853
  flex: 1 1 0%;
813
854
  }
814
855
 
856
+ .flex-auto {
857
+ flex: 1 1 auto;
858
+ }
859
+
860
+ .flex-none {
861
+ flex: none;
862
+ }
863
+
815
864
  .flex-shrink-0 {
816
865
  flex-shrink: 0;
817
866
  }
@@ -844,6 +893,16 @@ video {
844
893
  list-style-type: disc;
845
894
  }
846
895
 
896
+ .appearance-none {
897
+ -webkit-appearance: none;
898
+ -moz-appearance: none;
899
+ appearance: none;
900
+ }
901
+
902
+ .grid-cols-1 {
903
+ grid-template-columns: repeat(1, minmax(0, 1fr));
904
+ }
905
+
847
906
  .flex-row {
848
907
  flex-direction: row;
849
908
  }
@@ -860,6 +919,10 @@ video {
860
919
  justify-content: center;
861
920
  }
862
921
 
922
+ .justify-between {
923
+ justify-content: space-between;
924
+ }
925
+
863
926
  .gap-1 {
864
927
  gap: 0.25rem;
865
928
  }
@@ -876,6 +939,16 @@ video {
876
939
  gap: 1rem;
877
940
  }
878
941
 
942
+ .gap-x-4 {
943
+ -moz-column-gap: 1rem;
944
+ column-gap: 1rem;
945
+ }
946
+
947
+ .gap-x-6 {
948
+ -moz-column-gap: 1.5rem;
949
+ column-gap: 1.5rem;
950
+ }
951
+
879
952
  .gap-y-5 {
880
953
  row-gap: 1.25rem;
881
954
  }
@@ -896,12 +969,23 @@ video {
896
969
  margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
897
970
  }
898
971
 
972
+ .space-y-4 > :not([hidden]) ~ :not([hidden]) {
973
+ --tw-space-y-reverse: 0;
974
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
975
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
976
+ }
977
+
899
978
  .divide-y > :not([hidden]) ~ :not([hidden]) {
900
979
  --tw-divide-y-reverse: 0;
901
980
  border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
902
981
  border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
903
982
  }
904
983
 
984
+ .divide-gray-100 > :not([hidden]) ~ :not([hidden]) {
985
+ --tw-divide-opacity: 1;
986
+ border-color: rgb(243 244 246 / var(--tw-divide-opacity));
987
+ }
988
+
905
989
  .divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
906
990
  --tw-divide-opacity: 1;
907
991
  border-color: rgb(229 231 235 / var(--tw-divide-opacity));
@@ -912,10 +996,18 @@ video {
912
996
  border-color: rgb(209 213 219 / var(--tw-divide-opacity));
913
997
  }
914
998
 
999
+ .self-center {
1000
+ align-self: center;
1001
+ }
1002
+
915
1003
  .self-stretch {
916
1004
  align-self: stretch;
917
1005
  }
918
1006
 
1007
+ .justify-self-end {
1008
+ justify-self: end;
1009
+ }
1010
+
919
1011
  .overflow-auto {
920
1012
  overflow: auto;
921
1013
  }
@@ -1041,6 +1133,11 @@ video {
1041
1133
  background-color: rgb(156 163 175 / var(--tw-bg-opacity));
1042
1134
  }
1043
1135
 
1136
+ .bg-gray-50 {
1137
+ --tw-bg-opacity: 1;
1138
+ background-color: rgb(249 250 251 / var(--tw-bg-opacity));
1139
+ }
1140
+
1044
1141
  .bg-gray-500 {
1045
1142
  --tw-bg-opacity: 1;
1046
1143
  background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -1186,6 +1283,16 @@ video {
1186
1283
  padding-bottom: 1rem;
1187
1284
  }
1188
1285
 
1286
+ .py-5 {
1287
+ padding-top: 1.25rem;
1288
+ padding-bottom: 1.25rem;
1289
+ }
1290
+
1291
+ .py-6 {
1292
+ padding-top: 1.5rem;
1293
+ padding-bottom: 1.5rem;
1294
+ }
1295
+
1189
1296
  .pb-2 {
1190
1297
  padding-bottom: 0.5rem;
1191
1298
  }
@@ -1222,6 +1329,10 @@ video {
1222
1329
  padding-right: 1rem;
1223
1330
  }
1224
1331
 
1332
+ .pr-8 {
1333
+ padding-right: 2rem;
1334
+ }
1335
+
1225
1336
  .pt-1 {
1226
1337
  padding-top: 0.25rem;
1227
1338
  }
@@ -1261,6 +1372,11 @@ video {
1261
1372
  line-height: 1.25rem;
1262
1373
  }
1263
1374
 
1375
+ .text-sm\/6 {
1376
+ font-size: 0.875rem;
1377
+ line-height: 1.5rem;
1378
+ }
1379
+
1264
1380
  .text-xl {
1265
1381
  font-size: 1.25rem;
1266
1382
  line-height: 1.75rem;
@@ -1271,6 +1387,11 @@ video {
1271
1387
  line-height: 1rem;
1272
1388
  }
1273
1389
 
1390
+ .text-xs\/5 {
1391
+ font-size: 0.75rem;
1392
+ line-height: 1.25rem;
1393
+ }
1394
+
1274
1395
  .font-bold {
1275
1396
  font-weight: 700;
1276
1397
  }
@@ -1367,10 +1488,22 @@ video {
1367
1488
  outline-offset: 2px;
1368
1489
  }
1369
1490
 
1491
+ .outline {
1492
+ outline-style: solid;
1493
+ }
1494
+
1370
1495
  .outline-0 {
1371
1496
  outline-width: 0px;
1372
1497
  }
1373
1498
 
1499
+ .outline-1 {
1500
+ outline-width: 1px;
1501
+ }
1502
+
1503
+ .-outline-offset-1 {
1504
+ outline-offset: -1px;
1505
+ }
1506
+
1374
1507
  .outline-gray-300 {
1375
1508
  outline-color: #d1d5db;
1376
1509
  }
@@ -1394,6 +1527,10 @@ video {
1394
1527
  --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity));
1395
1528
  }
1396
1529
 
1530
+ .ring-gray-600\/20 {
1531
+ --tw-ring-color: rgb(75 85 99 / 0.2);
1532
+ }
1533
+
1397
1534
  .ring-gray-900\/5 {
1398
1535
  --tw-ring-color: rgb(17 24 39 / 0.05);
1399
1536
  }
@@ -1445,6 +1582,11 @@ video {
1445
1582
  background-color: rgb(37 99 235 / var(--tw-bg-opacity));
1446
1583
  }
1447
1584
 
1585
+ .hover\:bg-forest-green-500:hover {
1586
+ --tw-bg-opacity: 1;
1587
+ background-color: rgb(0 242 58 / var(--tw-bg-opacity));
1588
+ }
1589
+
1448
1590
  .hover\:bg-gray-300:hover {
1449
1591
  --tw-bg-opacity: 1;
1450
1592
  background-color: rgb(209 213 219 / var(--tw-bg-opacity));
@@ -1519,6 +1661,30 @@ video {
1519
1661
  outline-offset: 2px;
1520
1662
  }
1521
1663
 
1664
+ .focus\:outline:focus {
1665
+ outline-style: solid;
1666
+ }
1667
+
1668
+ .focus\:outline-2:focus {
1669
+ outline-width: 2px;
1670
+ }
1671
+
1672
+ .focus\:-outline-offset-2:focus {
1673
+ outline-offset: -2px;
1674
+ }
1675
+
1676
+ .focus\:outline-indigo-600:focus {
1677
+ outline-color: #4f46e5;
1678
+ }
1679
+
1680
+ .focus\:outline-ultramarine-600:focus {
1681
+ outline-color: #1823ff;
1682
+ }
1683
+
1684
+ .focus\:outline-ultramarine-50:focus {
1685
+ outline-color: #f1f5ff;
1686
+ }
1687
+
1522
1688
  .focus\:ring-0:focus {
1523
1689
  --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1524
1690
  --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -1531,11 +1697,22 @@ video {
1531
1697
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1532
1698
  }
1533
1699
 
1700
+ .focus\:ring-1:focus {
1701
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
1702
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
1703
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
1704
+ }
1705
+
1534
1706
  .focus\:ring-blue-500:focus {
1535
1707
  --tw-ring-opacity: 1;
1536
1708
  --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
1537
1709
  }
1538
1710
 
1711
+ .focus\:ring-forest-green-500:focus {
1712
+ --tw-ring-opacity: 1;
1713
+ --tw-ring-color: rgb(0 242 58 / var(--tw-ring-opacity));
1714
+ }
1715
+
1539
1716
  .focus\:ring-gray-500:focus {
1540
1717
  --tw-ring-opacity: 1;
1541
1718
  --tw-ring-color: rgb(107 114 128 / var(--tw-ring-opacity));
@@ -1556,10 +1733,24 @@ video {
1556
1733
  --tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity));
1557
1734
  }
1558
1735
 
1736
+ .focus\:ring-ultramarine-50:focus {
1737
+ --tw-ring-opacity: 1;
1738
+ --tw-ring-color: rgb(241 245 255 / var(--tw-ring-opacity));
1739
+ }
1740
+
1741
+ .focus\:ring-ultramarine-200:focus {
1742
+ --tw-ring-opacity: 1;
1743
+ --tw-ring-color: rgb(206 218 255 / var(--tw-ring-opacity));
1744
+ }
1745
+
1559
1746
  .focus\:ring-offset-2:focus {
1560
1747
  --tw-ring-offset-width: 2px;
1561
1748
  }
1562
1749
 
1750
+ .focus\:ring-offset-0:focus {
1751
+ --tw-ring-offset-width: 0px;
1752
+ }
1753
+
1563
1754
  .focus\:ring-offset-gray-800:focus {
1564
1755
  --tw-ring-offset-color: #1f2937;
1565
1756
  }
@@ -1622,6 +1813,11 @@ video {
1622
1813
  display: flex;
1623
1814
  }
1624
1815
 
1816
+ .sm\:size-4 {
1817
+ width: 1rem;
1818
+ height: 1rem;
1819
+ }
1820
+
1625
1821
  .sm\:flex-auto {
1626
1822
  flex: 1 1 auto;
1627
1823
  }
@@ -1630,6 +1826,14 @@ video {
1630
1826
  flex: none;
1631
1827
  }
1632
1828
 
1829
+ .sm\:flex-col {
1830
+ flex-direction: column;
1831
+ }
1832
+
1833
+ .sm\:items-end {
1834
+ align-items: flex-end;
1835
+ }
1836
+
1633
1837
  .sm\:items-center {
1634
1838
  align-items: center;
1635
1839
  }
@@ -1649,6 +1853,10 @@ video {
1649
1853
  padding-right: 1.5rem;
1650
1854
  }
1651
1855
 
1856
+ .sm\:pl-0 {
1857
+ padding-left: 0px;
1858
+ }
1859
+
1652
1860
  .sm\:pl-6 {
1653
1861
  padding-left: 1.5rem;
1654
1862
  }
@@ -1662,6 +1870,11 @@ video {
1662
1870
  line-height: 1.25rem;
1663
1871
  }
1664
1872
 
1873
+ .sm\:text-sm\/6 {
1874
+ font-size: 0.875rem;
1875
+ line-height: 1.5rem;
1876
+ }
1877
+
1665
1878
  .sm\:leading-6 {
1666
1879
  line-height: 1.5rem;
1667
1880
  }
@@ -45,6 +45,8 @@ require('./modal/modal')(app);
45
45
  require('./models/models')(app);
46
46
  require('./navbar/navbar')(app);
47
47
  require('./splash/splash')(app);
48
+ require('./team/team')(app);
49
+ require('./team/new-invitation/new-invitation')(app);
48
50
 
49
51
  app.component('app-component', {
50
52
  template: `
@@ -53,7 +55,7 @@ app.component('app-component', {
53
55
  <splash />
54
56
  </div>
55
57
  <div v-else-if="!hasAPIKey || user">
56
- <navbar :user="user" />
58
+ <navbar :user="user" :roles="roles" />
57
59
  <div class="view">
58
60
  <router-view :key="$route.fullPath" />
59
61
  </div>
@@ -79,14 +81,17 @@ app.component('app-component', {
79
81
  if (mothership.hasAPIKey) {
80
82
  const token = window.localStorage.getItem('_mongooseStudioAccessToken');
81
83
  if (token) {
82
- this.user = await mothership.me().then(res => res.user);
84
+ const { user, roles } = await mothership.me();
85
+ this.user = user;
86
+ this.roles = roles;
83
87
  }
84
88
  }
85
89
  },
86
90
  setup() {
87
91
  const user = Vue.ref(null);
92
+ const roles = Vue.ref(null);
88
93
 
89
- const state = Vue.reactive({ user });
94
+ const state = Vue.reactive({ user, roles });
90
95
  Vue.provide('state', state);
91
96
 
92
97
  return state;
@@ -43,7 +43,7 @@
43
43
 
44
44
  .models .documents table th {
45
45
  position: sticky;
46
- top: 0px;
46
+ top: 42px;
47
47
  background-color: white;
48
48
  z-index: 1;
49
49
  }
@@ -90,9 +90,12 @@ td {
90
90
  }
91
91
 
92
92
  .models .documents-menu {
93
+ position: fixed;
94
+ background-color: white;
95
+ z-index: 1;
96
+ padding: 4px;
93
97
  display: flex;
94
- margin: 0.25em;
95
- width: calc(100vw - 220px);
98
+ width: calc(100vw - 12rem);
96
99
  }
97
100
 
98
101
  .models .documents-menu .search-input {
@@ -1,6 +1,6 @@
1
1
  <div class="models">
2
2
  <div>
3
- <div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)]">
3
+ <div class="flex grow flex-col gap-y-5 overflow-auto border-r border-gray-200 bg-white px-2 h-[calc(100vh-55px)] w-48">
4
4
  <div class="flex font-bold font-xl mt-4 pl-2">
5
5
  Models
6
6
  </div>
@@ -11,7 +11,7 @@
11
11
  <li v-for="model in models">
12
12
  <router-link
13
13
  :to="'/model/' + model"
14
- class="block rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700"
14
+ class="block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700"
15
15
  :class="model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'">
16
16
  {{model}}
17
17
  </router-link>
@@ -24,11 +24,11 @@
24
24
 
25
25
  </div>
26
26
  <div class="documents" ref="documentsList">
27
- <div>
27
+ <div class="relative h-[42px]">
28
28
  <div class="documents-menu">
29
29
  <div class="flex flex-row items-center w-full gap-2">
30
30
  <form @submit.prevent="search" class="flex-grow m-0">
31
- <input class="w-full rounded-md p-1 outline-gray-300 text-lg" type="text" placeholder="Filter or text" v-model="searchText" />
31
+ <input class="w-full rounded-md p-1 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none" type="text" placeholder="Filter or text" v-model="searchText" />
32
32
  </form>
33
33
  <div>
34
34
  <span v-if="status === 'loading'">Loading ...</span>
@@ -71,7 +71,7 @@
71
71
  </div>
72
72
  </div>
73
73
  </div>
74
- <div class="documents-container">
74
+ <div class="documents-container relative">
75
75
  <table v-if="outputType === 'table'">
76
76
  <thead>
77
77
  <th v-for="path in filteredPaths">
@@ -105,37 +105,38 @@
105
105
  <img src="images/loader.gif">
106
106
  </div>
107
107
  </div>
108
- <modal v-if="shouldShowExportModal">
109
- <template v-slot:body>
110
- <div class="modal-exit" @click="shouldShowExportModal = false">&times;</div>
111
- <export-query-results
112
- :schemaPaths="schemaPaths"
113
- :filter="filter"
114
- :currentModel="currentModel"
115
- @done="shouldShowExportModal = false">
116
- </export-query-results>
117
- </template>
118
- </modal>
119
- <modal v-if="shouldShowFieldModal">
120
- <template v-slot:body>
121
- <div class="modal-exit" @click="shouldShowFieldModal = false; selectedPaths = [...filteredPaths];">&times;</div>
122
- <div v-for="(path, index) in schemaPaths" :key="index" style="margin-bottom: 0.5em">
123
- <input type="checkbox" :id="'path.path'+index" @change="addOrRemove(path)" :value="path.path" :checked="isSelected(path.path)" />
124
- <label :for="'path' + index">{{path.path}}</label>
125
- </div>
126
- <div style="margin-top: 1em">
127
- <button type="submit" @click="filterDocuments()" style="color: black;margin-right: 0.5em">Filter Selection</button>
128
- <button type="submit" @click="deselectAll()" class="gray" style="margin-right: 0.5em">Deselect All</button>
129
- <button type="submit" @click="resetDocuments()" class="gray">Cancel</button>
130
-
131
- </div>
132
- </template>
133
- </modal>
134
- <modal v-if="shouldShowCreateModal">
135
- <template v-slot:body>
136
- <div class="modal-exit" @click="shouldShowCreateModal = false;">&times;</div>
137
- <create-document :currentModel="currentModel" :paths="schemaPaths" @close="closeCreationModal"></create-document>
138
- </template>
139
- </modal>
140
108
  </div>
109
+
110
+ <modal v-if="shouldShowExportModal">
111
+ <template v-slot:body>
112
+ <div class="modal-exit" @click="shouldShowExportModal = false">&times;</div>
113
+ <export-query-results
114
+ :schemaPaths="schemaPaths"
115
+ :filter="filter"
116
+ :currentModel="currentModel"
117
+ @done="shouldShowExportModal = false">
118
+ </export-query-results>
119
+ </template>
120
+ </modal>
121
+ <modal v-if="shouldShowFieldModal">
122
+ <template v-slot:body>
123
+ <div class="modal-exit" @click="shouldShowFieldModal = false; selectedPaths = [...filteredPaths];">&times;</div>
124
+ <div v-for="(path, index) in schemaPaths" :key="index" style="margin-bottom: 0.5em">
125
+ <input type="checkbox" :id="'path.path'+index" @change="addOrRemove(path)" :value="path.path" :checked="isSelected(path.path)" />
126
+ <label :for="'path' + index">{{path.path}}</label>
127
+ </div>
128
+ <div style="margin-top: 1em">
129
+ <button type="submit" @click="filterDocuments()" style="color: black;margin-right: 0.5em">Filter Selection</button>
130
+ <button type="submit" @click="deselectAll()" class="gray" style="margin-right: 0.5em">Deselect All</button>
131
+ <button type="submit" @click="resetDocuments()" class="gray">Cancel</button>
132
+
133
+ </div>
134
+ </template>
135
+ </modal>
136
+ <modal v-if="shouldShowCreateModal">
137
+ <template v-slot:body>
138
+ <div class="modal-exit" @click="shouldShowCreateModal = false;">&times;</div>
139
+ <create-document :currentModel="currentModel" :paths="schemaPaths" @close="closeCreationModal"></create-document>
140
+ </template>
141
+ </modal>
141
142
  </div>
@@ -20,6 +20,14 @@ exports.githubLogin = function githubLogin() {
20
20
  return client.post('/githubLogin', { state: window.location.href }).then(res => res.data);
21
21
  };
22
22
 
23
+ exports.getWorkspaceTeam = function getWorkspaceTeam() {
24
+ return client.post('/getWorkspaceTeam', { workspaceId: config__workspace._id }).then(res => res.data);
25
+ };
26
+
27
+ exports.inviteToWorkspace = function inviteToWorkspace(params) {
28
+ return client.post('/inviteToWorkspace', { workspaceId: config__workspace._id, ...params }).then(res => res.data);
29
+ };
30
+
23
31
  exports.github = function github(code) {
24
32
  return client.post('/github', { code, workspaceId: config__workspace._id }).then(res => res.data);
25
33
  };
@@ -35,8 +35,8 @@
35
35
  </button>
36
36
  </div>
37
37
 
38
- <div v-if="showFlyout" class="absolute right-0 z-10 top-[82%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
39
- <!-- Active: "bg-gray-100 outline-none", Not Active: "" -->
38
+ <div v-if="showFlyout" class="absolute right-0 z-10 top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
39
+ <router-link to="team" v-if="canViewTeam" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Team</router-link>
40
40
  <span @click="logout" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Sign out</span>
41
41
  </div>
42
42
  </div>
@@ -10,7 +10,7 @@ appendCSS(require('./navbar.css'));
10
10
 
11
11
  module.exports = app => app.component('navbar', {
12
12
  template: template,
13
- props: ['user'],
13
+ props: ['user', 'roles'],
14
14
  data: () => ({ nodeEnv: null, showFlyout: false }),
15
15
  computed: {
16
16
  routeName() {
@@ -21,6 +21,9 @@ module.exports = app => app.component('navbar', {
21
21
  },
22
22
  hasAPIKey() {
23
23
  return mothership.hasAPIKey;
24
+ },
25
+ canViewTeam() {
26
+ return this.roles?.includes('owner') || this.roles?.includes('admin');
24
27
  }
25
28
  },
26
29
  async mounted() {
@@ -25,5 +25,10 @@ module.exports = [
25
25
  path: '/dashboard/:dashboardId',
26
26
  name: 'dashboard',
27
27
  component: 'dashboard'
28
+ },
29
+ {
30
+ path: '/team',
31
+ name: 'team',
32
+ component: 'team'
28
33
  }
29
- ];
34
+ ];
@@ -0,0 +1,42 @@
1
+ <div class="p-1">
2
+ <form class="space-y-4">
3
+ <div class="text-lg font-bold">
4
+ New Invitation
5
+ </div>
6
+
7
+ <div>
8
+ <label for="githubUsername" class="block text-sm/6 font-medium text-gray-900">GitHub Username</label>
9
+ <div class="mt-2">
10
+ <input type="githubUsername" name="githubUsername" id="githubUsername" v-model="githubUsername" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6" placeholder="johnsmith12">
11
+ </div>
12
+ </div>
13
+
14
+ <div>
15
+ <label for="email" class="block text-sm/6 font-medium text-gray-900">Email (Optional)</label>
16
+ <div class="mt-2">
17
+ <input type="email" name="email" id="email" v-model="email" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6" placeholder="you@example.com">
18
+ </div>
19
+ </div>
20
+
21
+ <div>
22
+ <label for="location" class="block text-sm/6 font-medium text-gray-900">Role</label>
23
+ <div class="mt-2 grid grid-cols-1">
24
+ <select id="role" name="role" v-model="role" class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6">
25
+ <option value="admin">Admin</option>
26
+ <option value="member">Member</option>
27
+ <option value="readonly">Read-only</option>
28
+ </select>
29
+ <svg class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
30
+ <path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
31
+ </svg>
32
+ </div>
33
+ </div>
34
+
35
+ <async-button
36
+ type="submit"
37
+ @click="inviteToWorkspace"
38
+ class="inline-flex justify-center rounded-md border border-transparent bg-forest-green-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-forest-green-500 focus:outline-none focus:ring-2 focus:ring-forest-green-500 focus:ring-offset-2">
39
+ Submit
40
+ </async-button>
41
+ </form>
42
+ </div>
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const mothership = require('../../mothership');
4
+ const template = require('./new-invitation.html');
5
+
6
+ module.exports = app => app.component('new-invitation', {
7
+ template,
8
+ emits: ['close', 'invitationCreated'],
9
+ data: () => ({
10
+ githubUsername: '',
11
+ email: '',
12
+ role: null
13
+ }),
14
+ methods: {
15
+ async inviteToWorkspace() {
16
+ const { invitation } = await mothership.inviteToWorkspace({ githubUsername: this.githubUsername, email: this.email, roles: [this.role] });
17
+ this.$emit('invitationCreated', { invitation });
18
+ this.$emit('close');
19
+ }
20
+ }
21
+ });
@@ -0,0 +1,86 @@
1
+ <div class="mx-auto max-w-5xl py-6 px-2">
2
+ <div class="text-xl font-bold">
3
+ Current Members
4
+ </div>
5
+ <ul role="list" class="divide-y divide-gray-100">
6
+ <li class="flex justify-between gap-x-6 py-5" v-for="user in users">
7
+ <div class="flex min-w-0 gap-x-4">
8
+ <img class="size-12 flex-none rounded-full bg-gray-50" :src="user.picture ?? 'images/logo.svg'" alt="">
9
+ <div class="min-w-0 flex-auto">
10
+ <p class="text-sm/6 font-semibold text-gray-900">{{user.name}}</p>
11
+ <p class="mt-1 truncate text-xs/5 text-gray-500">{{user.email}}</p>
12
+ </div>
13
+ </div>
14
+ <div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
15
+ <p class="text-sm/6 text-gray-900 capitalize">{{getRolesForUser(user).join(', ')}}</p>
16
+ <p class="mt-1 text-xs/5 text-gray-500">Last seen <time datetime="2023-01-23T13:23Z">3h ago</time></p>
17
+ </div>
18
+ </li>
19
+ </ul>
20
+ <div class="mt-6">
21
+ <div class="flex items-center justify-between">
22
+ <div class="text-xl font-bold">
23
+ Invitations
24
+ </div>
25
+ <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
26
+ <button
27
+ type="button"
28
+ @click="showNewInvitationModal = true"
29
+ class="block rounded-md bg-ultramarine-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600">
30
+ New Invitation
31
+ </button>
32
+ </div>
33
+ </div>
34
+ <div class="mt-8 flow-root" v-if="invitations?.length > 0">
35
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
36
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
37
+ <table class="min-w-full divide-y divide-gray-300">
38
+ <thead>
39
+ <tr>
40
+ <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">GitHub Username</th>
41
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Email</th>
42
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
43
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
44
+ </tr>
45
+ </thead>
46
+ <tbody class="divide-y divide-gray-200 bg-white">
47
+ <tr v-for="invitation in invitations">
48
+ <td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0">
49
+ {{invitation.githubUsername}}
50
+ </td>
51
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
52
+ {{invitation.email}}
53
+ </td>
54
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
55
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-600/20">
56
+ Pending
57
+ </span>
58
+ </td>
59
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500">
60
+ {{invitation.roles.join(', ')}}
61
+ </td>
62
+ </tr>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div v-if="invitations?.length === 0" class="mt-4">
70
+ <div class="text-center">
71
+ <svg class="mx-auto size-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
72
+ <path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
73
+ </svg>
74
+ <h3 class="mt-2 text-sm font-semibold text-gray-900">No invitations</h3>
75
+ <p class="mt-1 text-sm text-gray-500">You have no outstanding invitations</p>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <modal v-if="showNewInvitationModal">
81
+ <template v-slot:body>
82
+ <div class="modal-exit" @click="showNewInvitationModal = false">&times;</div>
83
+ <new-invitation @close="showNewInvitationModal = false" @invitationCreated="invitations.push($event.invitation)"></new-invitation>
84
+ </template>
85
+ </modal>
86
+ </div>
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const mothership = require('../mothership');
4
+ const template = require('./team.html');
5
+
6
+ module.exports = app => app.component('team', {
7
+ template,
8
+ data: () => ({
9
+ workspace: null,
10
+ users: null,
11
+ invitations: null,
12
+ showNewInvitationModal: false
13
+ }),
14
+ async mounted() {
15
+ const { workspace, users, invitations } = await mothership.getWorkspaceTeam();
16
+ this.workspace = workspace;
17
+ this.users = users;
18
+ this.invitations = invitations;
19
+ },
20
+ methods: {
21
+ getRolesForUser(user) {
22
+ return this.workspace.members.find(member => member.userId === user._id)?.roles ?? [];
23
+ }
24
+ }
25
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "dependencies": {
5
5
  "archetype": "0.13.1",
6
6
  "csv-stringify": "6.3.0",
package/test.html ADDED
@@ -0,0 +1,57 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fixed Navbars with Scrollable Table</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ </head>
9
+ <body class="h-screen flex flex-col">
10
+
11
+ <!-- Top-Level Navbar (Fixed) -->
12
+ <nav class="bg-blue-600 text-white p-4 fixed top-0 left-0 right-0 z-20">
13
+ Top-Level Navbar
14
+ </nav>
15
+
16
+ <!-- Side Navbar (Fixed) -->
17
+ <aside class="bg-gray-800 text-white w-64 p-4 fixed top-16 left-0 bottom-0 z-10">
18
+ Side Navbar
19
+ </aside>
20
+
21
+ <!-- Stacked Navigation Bar (Fixed Below Top Navbar) -->
22
+ <nav class="bg-gray-300 p-4 fixed top-16 left-64 right-0 z-20">
23
+ Stacked Navigation Bar
24
+ </nav>
25
+
26
+ <!-- Scrollable Table Container -->
27
+ <div class="flex-grow overflow-auto p-4 pl-64 pt-32 bg-gray-50" style="height: calc(100vh - 8rem);">
28
+ <div class="overflow-auto border border-gray-300" style="max-height: calc(100vh - 12rem);">
29
+ <table class="min-w-max border border-gray-300">
30
+ <thead class="bg-gray-300 sticky top-0 z-10">
31
+ <tr>
32
+ <!-- Generate many columns -->
33
+ <script>
34
+ for (let j = 0; j < 20; j++) {
35
+ document.write(`<th class="border p-2 whitespace-nowrap">Header ${j + 1}</th>`);
36
+ }
37
+ </script>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <!-- Generate many rows -->
42
+ <script>
43
+ for (let i = 0; i < 100; i++) {
44
+ document.write(`<tr class="border">`);
45
+ for (let j = 0; j < 20; j++) {
46
+ document.write(`<td class="border p-2 whitespace-nowrap">Row ${i + 1} Col ${j + 1}</td>`);
47
+ }
48
+ document.write(`</tr>`);
49
+ }
50
+ </script>
51
+ </tbody>
52
+ </table>
53
+ </div>
54
+ </div>
55
+
56
+ </body>
57
+ </html>